@darksol/terminal 0.1.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.
@@ -0,0 +1,718 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { ethers } from 'ethers';
5
+ import { decryptKey, loadWallet, listWallets } from '../wallet/keystore.js';
6
+ import { getConfig, getRPC } from '../config/store.js';
7
+ import { resolveToken } from '../trading/swap.js';
8
+ import { theme } from '../ui/theme.js';
9
+ import { spinner, kvDisplay, success, error, warn, table, info } from '../ui/components.js';
10
+ import { showSection, showDivider } from '../ui/banner.js';
11
+ import inquirer from 'inquirer';
12
+
13
+ const SCRIPTS_DIR = join(homedir(), '.darksol', 'scripts');
14
+
15
+ function ensureDir() {
16
+ if (!existsSync(SCRIPTS_DIR)) mkdirSync(SCRIPTS_DIR, { recursive: true });
17
+ }
18
+
19
+ // ──────────────────────────────────────────────────
20
+ // SCRIPT TEMPLATES
21
+ // ──────────────────────────────────────────────────
22
+
23
+ const TEMPLATES = {
24
+ 'buy-token': {
25
+ name: 'Buy Token',
26
+ description: 'Buy a token with ETH at current price',
27
+ params: ['token', 'amountETH'],
28
+ template: `// Buy Token Script
29
+ // Buys {token} with {amountETH} ETH via Uniswap V2
30
+ module.exports = async function({ signer, provider, ethers, config, params }) {
31
+ const WETH = '0x4200000000000000000000000000000000000006';
32
+ const ROUTER = '0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24';
33
+
34
+ const router = new ethers.Contract(ROUTER, [
35
+ 'function swapExactETHForTokens(uint amountOutMin, address[] path, address to, uint deadline) payable returns (uint[] amounts)',
36
+ 'function getAmountsOut(uint amountIn, address[] path) view returns (uint[] amounts)',
37
+ ], signer);
38
+
39
+ const amountIn = ethers.parseEther(params.amountETH);
40
+ const path = [WETH, params.token];
41
+ const deadline = Math.floor(Date.now() / 1000) + 300;
42
+
43
+ // Get estimated output
44
+ const amounts = await router.getAmountsOut(amountIn, path);
45
+ const minOut = (amounts[1] * 95n) / 100n; // 5% slippage
46
+
47
+ console.log('Estimated output:', ethers.formatUnits(amounts[1], 18));
48
+ console.log('Min output (5% slippage):', ethers.formatUnits(minOut, 18));
49
+
50
+ const tx = await router.swapExactETHForTokens(minOut, path, signer.address, deadline, { value: amountIn });
51
+ const receipt = await tx.wait();
52
+
53
+ return { txHash: receipt.hash, block: receipt.blockNumber, gasUsed: receipt.gasUsed.toString() };
54
+ };`,
55
+ },
56
+
57
+ 'sell-token': {
58
+ name: 'Sell Token',
59
+ description: 'Sell a token for ETH',
60
+ params: ['token', 'amountPercent'],
61
+ template: `// Sell Token Script
62
+ // Sells {amountPercent}% of {token} balance for ETH
63
+ module.exports = async function({ signer, provider, ethers, config, params }) {
64
+ const WETH = '0x4200000000000000000000000000000000000006';
65
+ const ROUTER = '0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24';
66
+
67
+ const token = new ethers.Contract(params.token, [
68
+ 'function balanceOf(address) view returns (uint256)',
69
+ 'function approve(address, uint256) returns (bool)',
70
+ 'function decimals() view returns (uint8)',
71
+ 'function symbol() view returns (string)',
72
+ ], signer);
73
+
74
+ const balance = await token.balanceOf(signer.address);
75
+ const decimals = await token.decimals();
76
+ const symbol = await token.symbol();
77
+ const sellAmount = (balance * BigInt(params.amountPercent)) / 100n;
78
+
79
+ console.log('Token:', symbol);
80
+ console.log('Balance:', ethers.formatUnits(balance, decimals));
81
+ console.log('Selling:', ethers.formatUnits(sellAmount, decimals), '(' + params.amountPercent + '%)');
82
+
83
+ // Approve
84
+ const approveTx = await token.approve(ROUTER, sellAmount);
85
+ await approveTx.wait();
86
+
87
+ const router = new ethers.Contract(ROUTER, [
88
+ 'function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] path, address to, uint deadline) returns (uint[] amounts)',
89
+ ], signer);
90
+
91
+ const deadline = Math.floor(Date.now() / 1000) + 300;
92
+ const tx = await router.swapExactTokensForETH(sellAmount, 0, [params.token, WETH], signer.address, deadline);
93
+ const receipt = await tx.wait();
94
+
95
+ return { txHash: receipt.hash, block: receipt.blockNumber, gasUsed: receipt.gasUsed.toString() };
96
+ };`,
97
+ },
98
+
99
+ 'limit-buy': {
100
+ name: 'Limit Buy',
101
+ description: 'Buy token when price drops to target (polling)',
102
+ params: ['token', 'targetPrice', 'amountETH', 'pollSeconds'],
103
+ template: `// Limit Buy Script
104
+ // Watches {token} and buys with {amountETH} ETH when price <= {targetPrice}
105
+ module.exports = async function({ signer, provider, ethers, config, params }) {
106
+ const WETH = '0x4200000000000000000000000000000000000006';
107
+ const ROUTER = '0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24';
108
+ const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
109
+ const pollMs = (parseInt(params.pollSeconds) || 30) * 1000;
110
+
111
+ const router = new ethers.Contract(ROUTER, [
112
+ 'function swapExactETHForTokens(uint amountOutMin, address[] path, address to, uint deadline) payable returns (uint[] amounts)',
113
+ 'function getAmountsOut(uint amountIn, address[] path) view returns (uint[] amounts)',
114
+ ], signer);
115
+
116
+ console.log('Limit buy active — polling every', pollMs/1000, 'seconds');
117
+ console.log('Target price: $' + params.targetPrice);
118
+
119
+ while (true) {
120
+ try {
121
+ // Check price via WETH->Token->USDC path estimate
122
+ const oneETH = ethers.parseEther('1');
123
+ const amounts = await router.getAmountsOut(oneETH, [WETH, params.token]);
124
+ const tokenPerETH = amounts[1];
125
+
126
+ // Rough price calc (assumes 18 decimals)
127
+ const priceEstimate = parseFloat(ethers.formatUnits(tokenPerETH, 18));
128
+ process.stdout.write('\\rPrice check: ~' + priceEstimate.toFixed(6) + ' tokens/ETH ');
129
+
130
+ if (priceEstimate >= parseFloat(params.targetPrice)) {
131
+ console.log('\\nTarget hit! Executing buy...');
132
+
133
+ const amountIn = ethers.parseEther(params.amountETH);
134
+ const deadline = Math.floor(Date.now() / 1000) + 300;
135
+ const minOut = (amounts[1] * BigInt(Math.floor(parseFloat(params.amountETH) * 1e18))) / oneETH;
136
+ const minOutSlipped = (minOut * 90n) / 100n;
137
+
138
+ const tx = await router.swapExactETHForTokens(minOutSlipped, [WETH, params.token], signer.address, deadline, { value: amountIn });
139
+ const receipt = await tx.wait();
140
+ return { txHash: receipt.hash, filled: true };
141
+ }
142
+ } catch (err) {
143
+ console.log('\\nPrice check error:', err.message);
144
+ }
145
+
146
+ await new Promise(r => setTimeout(r, pollMs));
147
+ }
148
+ };`,
149
+ },
150
+
151
+ 'stop-loss': {
152
+ name: 'Stop Loss',
153
+ description: 'Auto-sell token if value drops below threshold',
154
+ params: ['token', 'stopPrice', 'sellPercent', 'pollSeconds'],
155
+ template: `// Stop Loss Script
156
+ // Sells {sellPercent}% of {token} if price drops below {stopPrice}
157
+ module.exports = async function({ signer, provider, ethers, config, params }) {
158
+ const WETH = '0x4200000000000000000000000000000000000006';
159
+ const ROUTER = '0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24';
160
+ const pollMs = (parseInt(params.pollSeconds) || 15) * 1000;
161
+
162
+ const token = new ethers.Contract(params.token, [
163
+ 'function balanceOf(address) view returns (uint256)',
164
+ 'function approve(address, uint256) returns (bool)',
165
+ 'function decimals() view returns (uint8)',
166
+ 'function symbol() view returns (string)',
167
+ ], signer);
168
+
169
+ const router = new ethers.Contract(ROUTER, [
170
+ 'function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] path, address to, uint deadline) returns (uint[] amounts)',
171
+ 'function getAmountsOut(uint amountIn, address[] path) view returns (uint[] amounts)',
172
+ ], signer);
173
+
174
+ const symbol = await token.symbol();
175
+ const decimals = await token.decimals();
176
+ console.log('Stop loss active for', symbol);
177
+ console.log('Stop price: $' + params.stopPrice);
178
+ console.log('Will sell', params.sellPercent + '% on trigger');
179
+
180
+ while (true) {
181
+ try {
182
+ const balance = await token.balanceOf(signer.address);
183
+ if (balance === 0n) {
184
+ console.log('\\nNo token balance remaining. Exiting.');
185
+ return { triggered: false, reason: 'zero_balance' };
186
+ }
187
+
188
+ // Get price estimate
189
+ const testAmount = balance / 100n || 1n;
190
+ const amounts = await router.getAmountsOut(testAmount, [params.token, WETH]);
191
+ const ethValue = parseFloat(ethers.formatEther(amounts[1]));
192
+
193
+ process.stdout.write('\\rMonitoring ' + symbol + ' — value estimate active ');
194
+
195
+ // Simple threshold (this is approximate — production would use oracle)
196
+ if (ethValue < parseFloat(params.stopPrice)) {
197
+ console.log('\\n⚠ STOP LOSS TRIGGERED');
198
+
199
+ const sellAmount = (balance * BigInt(params.sellPercent)) / 100n;
200
+ await (await token.approve(ROUTER, sellAmount)).wait();
201
+
202
+ const deadline = Math.floor(Date.now() / 1000) + 120;
203
+ const tx = await router.swapExactTokensForETH(sellAmount, 0, [params.token, WETH], signer.address, deadline);
204
+ const receipt = await tx.wait();
205
+
206
+ return { triggered: true, txHash: receipt.hash, sold: ethers.formatUnits(sellAmount, decimals) };
207
+ }
208
+ } catch (err) {
209
+ console.log('\\nMonitor error:', err.message);
210
+ }
211
+
212
+ await new Promise(r => setTimeout(r, pollMs));
213
+ }
214
+ };`,
215
+ },
216
+
217
+ 'multi-buy': {
218
+ name: 'Multi Buy',
219
+ description: 'Buy multiple tokens in one execution',
220
+ params: ['tokens', 'amountETHEach'],
221
+ template: `// Multi Buy Script
222
+ // Buys multiple tokens, splitting ETH equally
223
+ // tokens param should be comma-separated addresses
224
+ module.exports = async function({ signer, provider, ethers, config, params }) {
225
+ const WETH = '0x4200000000000000000000000000000000000006';
226
+ const ROUTER = '0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24';
227
+ const tokens = params.tokens.split(',').map(t => t.trim());
228
+ const amountPerToken = ethers.parseEther(params.amountETHEach);
229
+
230
+ const router = new ethers.Contract(ROUTER, [
231
+ 'function swapExactETHForTokens(uint amountOutMin, address[] path, address to, uint deadline) payable returns (uint[] amounts)',
232
+ ], signer);
233
+
234
+ const results = [];
235
+
236
+ for (const tokenAddr of tokens) {
237
+ console.log('\\nBuying token:', tokenAddr);
238
+ try {
239
+ const deadline = Math.floor(Date.now() / 1000) + 300;
240
+ const tx = await router.swapExactETHForTokens(0, [WETH, tokenAddr], signer.address, deadline, { value: amountPerToken });
241
+ const receipt = await tx.wait();
242
+ results.push({ token: tokenAddr, txHash: receipt.hash, status: 'success' });
243
+ console.log('✓ Bought:', receipt.hash);
244
+ } catch (err) {
245
+ results.push({ token: tokenAddr, error: err.message, status: 'failed' });
246
+ console.log('✗ Failed:', err.message);
247
+ }
248
+ }
249
+
250
+ return { results, totalSpent: ethers.formatEther(amountPerToken * BigInt(tokens.length)) + ' ETH' };
251
+ };`,
252
+ },
253
+
254
+ 'transfer': {
255
+ name: 'Transfer',
256
+ description: 'Transfer ETH or tokens to another address',
257
+ params: ['to', 'amount', 'token'],
258
+ template: `// Transfer Script
259
+ // Sends {amount} of {token} (or ETH if token is 'ETH') to {to}
260
+ module.exports = async function({ signer, provider, ethers, config, params }) {
261
+ const toAddress = params.to;
262
+
263
+ if (!params.token || params.token.toUpperCase() === 'ETH') {
264
+ // ETH transfer
265
+ const value = ethers.parseEther(params.amount);
266
+ console.log('Sending', params.amount, 'ETH to', toAddress);
267
+ const tx = await signer.sendTransaction({ to: toAddress, value });
268
+ const receipt = await tx.wait();
269
+ return { txHash: receipt.hash, type: 'ETH', amount: params.amount };
270
+ } else {
271
+ // ERC20 transfer
272
+ const token = new ethers.Contract(params.token, [
273
+ 'function transfer(address, uint256) returns (bool)',
274
+ 'function decimals() view returns (uint8)',
275
+ 'function symbol() view returns (string)',
276
+ ], signer);
277
+
278
+ const decimals = await token.decimals();
279
+ const symbol = await token.symbol();
280
+ const amount = ethers.parseUnits(params.amount, decimals);
281
+
282
+ console.log('Sending', params.amount, symbol, 'to', toAddress);
283
+ const tx = await token.transfer(toAddress, amount);
284
+ const receipt = await tx.wait();
285
+ return { txHash: receipt.hash, type: symbol, amount: params.amount };
286
+ }
287
+ };`,
288
+ },
289
+
290
+ 'empty': {
291
+ name: 'Custom Script',
292
+ description: 'Empty template for custom logic',
293
+ params: [],
294
+ template: `// Custom DARKSOL Script
295
+ // Available in context: { signer, provider, ethers, config, params }
296
+ //
297
+ // signer — ethers.Wallet connected to provider (your unlocked wallet)
298
+ // provider — ethers.JsonRpcProvider for the active chain
299
+ // ethers — the ethers library
300
+ // config — { chain, slippage, gasMultiplier, rpcs, ... }
301
+ // params — your custom parameters (from script config)
302
+ //
303
+ // Return an object with results. Throw to signal failure.
304
+ module.exports = async function({ signer, provider, ethers, config, params }) {
305
+ console.log('Wallet:', signer.address);
306
+ console.log('Chain:', config.chain);
307
+
308
+ // Your logic here
309
+
310
+ return { status: 'ok' };
311
+ };`,
312
+ },
313
+ };
314
+
315
+ // ──────────────────────────────────────────────────
316
+ // SCRIPT MANAGEMENT
317
+ // ──────────────────────────────────────────────────
318
+
319
+ function getScriptPath(name) {
320
+ return join(SCRIPTS_DIR, `${name}.json`);
321
+ }
322
+
323
+ function loadScript(name) {
324
+ const path = getScriptPath(name);
325
+ if (!existsSync(path)) throw new Error(`Script "${name}" not found`);
326
+ return JSON.parse(readFileSync(path, 'utf8'));
327
+ }
328
+
329
+ function saveScript(script) {
330
+ ensureDir();
331
+ writeFileSync(getScriptPath(script.name), JSON.stringify(script, null, 2));
332
+ }
333
+
334
+ function getAllScripts() {
335
+ ensureDir();
336
+ return readdirSync(SCRIPTS_DIR)
337
+ .filter(f => f.endsWith('.json'))
338
+ .map(f => JSON.parse(readFileSync(join(SCRIPTS_DIR, f), 'utf8')));
339
+ }
340
+
341
+ // Create a new script from template or custom
342
+ export async function createScript(opts = {}) {
343
+ showSection('CREATE EXECUTION SCRIPT');
344
+
345
+ const templateChoices = Object.entries(TEMPLATES).map(([key, t]) => ({
346
+ name: `${t.name} — ${t.description}`,
347
+ value: key,
348
+ }));
349
+
350
+ const { templateKey } = await inquirer.prompt([{
351
+ type: 'list',
352
+ name: 'templateKey',
353
+ message: theme.gold('Select template:'),
354
+ choices: templateChoices,
355
+ }]);
356
+
357
+ const template = TEMPLATES[templateKey];
358
+
359
+ const { scriptName } = await inquirer.prompt([{
360
+ type: 'input',
361
+ name: 'scriptName',
362
+ message: theme.gold('Script name:'),
363
+ default: templateKey,
364
+ validate: v => /^[a-zA-Z0-9_-]+$/.test(v) || 'Alphanumeric, dashes, underscores only',
365
+ }]);
366
+
367
+ // Collect params
368
+ const paramValues = {};
369
+ for (const param of template.params) {
370
+ const { value } = await inquirer.prompt([{
371
+ type: 'input',
372
+ name: 'value',
373
+ message: theme.gold(`${param}:`),
374
+ }]);
375
+ paramValues[param] = value;
376
+ }
377
+
378
+ const { walletName } = await inquirer.prompt([{
379
+ type: 'input',
380
+ name: 'walletName',
381
+ message: theme.gold('Wallet to use:'),
382
+ default: getConfig('activeWallet') || '',
383
+ }]);
384
+
385
+ const script = {
386
+ name: scriptName,
387
+ template: templateKey,
388
+ description: template.description,
389
+ wallet: walletName,
390
+ chain: getConfig('chain') || 'base',
391
+ params: paramValues,
392
+ code: template.template,
393
+ createdAt: new Date().toISOString(),
394
+ lastRun: null,
395
+ runCount: 0,
396
+ };
397
+
398
+ saveScript(script);
399
+
400
+ console.log('');
401
+ success(`Script created: ${scriptName}`);
402
+ kvDisplay([
403
+ ['Name', scriptName],
404
+ ['Template', template.name],
405
+ ['Wallet', walletName],
406
+ ['Chain', script.chain],
407
+ ['Params', Object.entries(paramValues).map(([k, v]) => `${k}=${v}`).join(', ') || '(none)'],
408
+ ['Stored', getScriptPath(scriptName)],
409
+ ]);
410
+ console.log('');
411
+ info('Run with: darksol script run ' + scriptName);
412
+ info('Edit code: darksol script edit ' + scriptName);
413
+ }
414
+
415
+ // List all scripts
416
+ export function listScripts() {
417
+ const scripts = getAllScripts();
418
+
419
+ if (scripts.length === 0) {
420
+ warn('No scripts found. Create one with: darksol script create');
421
+ return;
422
+ }
423
+
424
+ showSection('EXECUTION SCRIPTS');
425
+
426
+ const rows = scripts.map(s => [
427
+ theme.gold(s.name),
428
+ s.description || s.template,
429
+ s.wallet || theme.dim('(default)'),
430
+ s.chain,
431
+ s.runCount.toString(),
432
+ s.lastRun ? new Date(s.lastRun).toLocaleString() : theme.dim('never'),
433
+ ]);
434
+
435
+ table(['Name', 'Type', 'Wallet', 'Chain', 'Runs', 'Last Run'], rows);
436
+ }
437
+
438
+ // Run a script
439
+ export async function runScript(name, opts = {}) {
440
+ let script;
441
+ try {
442
+ script = loadScript(name);
443
+ } catch {
444
+ error(`Script "${name}" not found`);
445
+ return;
446
+ }
447
+
448
+ showSection(`EXECUTING: ${name}`);
449
+ kvDisplay([
450
+ ['Script', script.name],
451
+ ['Type', script.template],
452
+ ['Wallet', script.wallet],
453
+ ['Chain', script.chain],
454
+ ['Params', Object.entries(script.params).map(([k, v]) => `${k}=${v}`).join(', ') || '(none)'],
455
+ ]);
456
+ console.log('');
457
+
458
+ // Get wallet password (unless --password provided for automation)
459
+ let password = opts.password;
460
+ if (!password) {
461
+ const { pw } = await inquirer.prompt([{
462
+ type: 'password',
463
+ name: 'pw',
464
+ message: theme.gold('Wallet password:'),
465
+ mask: '●',
466
+ }]);
467
+ password = pw;
468
+ }
469
+
470
+ // Confirm unless --yes flag
471
+ if (!opts.yes) {
472
+ const { confirm } = await inquirer.prompt([{
473
+ type: 'confirm',
474
+ name: 'confirm',
475
+ message: theme.accent('Execute script? This will use your private key for transactions.'),
476
+ default: false,
477
+ }]);
478
+ if (!confirm) {
479
+ warn('Execution cancelled');
480
+ return;
481
+ }
482
+ }
483
+
484
+ const spin = spinner('Unlocking wallet...').start();
485
+
486
+ try {
487
+ // Unlock wallet
488
+ const walletData = loadWallet(script.wallet || getConfig('activeWallet'));
489
+ const privateKey = decryptKey(walletData.keystore, password);
490
+ const rpcUrl = getRPC(script.chain);
491
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
492
+ const signer = new ethers.Wallet(privateKey, provider);
493
+
494
+ spin.text = 'Running script...';
495
+
496
+ // Build execution context
497
+ const context = {
498
+ signer,
499
+ provider,
500
+ ethers,
501
+ config: {
502
+ chain: script.chain,
503
+ slippage: getConfig('slippage'),
504
+ gasMultiplier: getConfig('gasMultiplier'),
505
+ rpcs: getConfig('rpcs'),
506
+ },
507
+ params: script.params,
508
+ };
509
+
510
+ // Execute the script
511
+ // We use Function constructor to run the script code in a sandboxed-ish context
512
+ // The script code uses module.exports pattern, so we wrap it
513
+ const wrappedCode = `
514
+ const module = { exports: null };
515
+ ${script.code}
516
+ return module.exports;
517
+ `;
518
+
519
+ const scriptFn = new Function(wrappedCode)();
520
+
521
+ if (typeof scriptFn !== 'function') {
522
+ throw new Error('Script must export an async function via module.exports');
523
+ }
524
+
525
+ const startTime = Date.now();
526
+ const result = await scriptFn(context);
527
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
528
+
529
+ spin.succeed(theme.success('Script completed'));
530
+
531
+ // Show results
532
+ console.log('');
533
+ showSection('RESULTS');
534
+ if (result && typeof result === 'object') {
535
+ kvDisplay(Object.entries(result).map(([k, v]) =>
536
+ [k, typeof v === 'object' ? JSON.stringify(v) : String(v)]
537
+ ));
538
+ }
539
+ console.log('');
540
+ info(`Execution time: ${elapsed}s`);
541
+
542
+ // Update script metadata
543
+ script.lastRun = new Date().toISOString();
544
+ script.runCount++;
545
+ saveScript(script);
546
+
547
+ } catch (err) {
548
+ spin.fail('Script failed');
549
+ error(err.message);
550
+ if (opts.verbose) {
551
+ console.log(theme.dim(err.stack));
552
+ }
553
+ }
554
+ }
555
+
556
+ // Show script details
557
+ export async function showScript(name) {
558
+ let script;
559
+ try {
560
+ script = loadScript(name);
561
+ } catch {
562
+ error(`Script "${name}" not found`);
563
+ return;
564
+ }
565
+
566
+ showSection(`SCRIPT: ${name}`);
567
+ kvDisplay([
568
+ ['Name', script.name],
569
+ ['Template', script.template],
570
+ ['Description', script.description],
571
+ ['Wallet', script.wallet],
572
+ ['Chain', script.chain],
573
+ ['Run Count', script.runCount.toString()],
574
+ ['Last Run', script.lastRun || 'never'],
575
+ ['Created', script.createdAt],
576
+ ]);
577
+
578
+ if (Object.keys(script.params).length > 0) {
579
+ console.log('');
580
+ showSection('PARAMETERS');
581
+ kvDisplay(Object.entries(script.params).map(([k, v]) => [k, v]));
582
+ }
583
+
584
+ console.log('');
585
+ showSection('CODE');
586
+ console.log(theme.dim(script.code));
587
+ }
588
+
589
+ // Edit script params
590
+ export async function editScript(name) {
591
+ let script;
592
+ try {
593
+ script = loadScript(name);
594
+ } catch {
595
+ error(`Script "${name}" not found`);
596
+ return;
597
+ }
598
+
599
+ showSection(`EDIT: ${name}`);
600
+
601
+ const { what } = await inquirer.prompt([{
602
+ type: 'list',
603
+ name: 'what',
604
+ message: theme.gold('What to edit:'),
605
+ choices: [
606
+ { name: 'Parameters', value: 'params' },
607
+ { name: 'Wallet', value: 'wallet' },
608
+ { name: 'Chain', value: 'chain' },
609
+ { name: 'Description', value: 'description' },
610
+ ],
611
+ }]);
612
+
613
+ if (what === 'params') {
614
+ for (const [key, currentVal] of Object.entries(script.params)) {
615
+ const { value } = await inquirer.prompt([{
616
+ type: 'input',
617
+ name: 'value',
618
+ message: theme.gold(`${key}:`),
619
+ default: currentVal,
620
+ }]);
621
+ script.params[key] = value;
622
+ }
623
+ } else if (what === 'wallet') {
624
+ const wallets = listWallets();
625
+ const { wallet } = await inquirer.prompt([{
626
+ type: 'list',
627
+ name: 'wallet',
628
+ message: theme.gold('Wallet:'),
629
+ choices: wallets.map(w => w.name),
630
+ }]);
631
+ script.wallet = wallet;
632
+ } else if (what === 'chain') {
633
+ const { chain } = await inquirer.prompt([{
634
+ type: 'list',
635
+ name: 'chain',
636
+ message: theme.gold('Chain:'),
637
+ choices: ['base', 'ethereum', 'polygon', 'arbitrum', 'optimism'],
638
+ }]);
639
+ script.chain = chain;
640
+ } else if (what === 'description') {
641
+ const { desc } = await inquirer.prompt([{
642
+ type: 'input',
643
+ name: 'desc',
644
+ message: theme.gold('Description:'),
645
+ default: script.description,
646
+ }]);
647
+ script.description = desc;
648
+ }
649
+
650
+ saveScript(script);
651
+ success('Script updated');
652
+ }
653
+
654
+ // Delete a script
655
+ export async function deleteScript(name) {
656
+ const path = getScriptPath(name);
657
+ if (!existsSync(path)) {
658
+ error(`Script "${name}" not found`);
659
+ return;
660
+ }
661
+
662
+ const { confirm } = await inquirer.prompt([{
663
+ type: 'confirm',
664
+ name: 'confirm',
665
+ message: theme.accent(`Delete script "${name}"?`),
666
+ default: false,
667
+ }]);
668
+
669
+ if (!confirm) return;
670
+
671
+ unlinkSync(path);
672
+ success(`Script "${name}" deleted`);
673
+ }
674
+
675
+ // Clone a script
676
+ export async function cloneScript(name, newName) {
677
+ let script;
678
+ try {
679
+ script = loadScript(name);
680
+ } catch {
681
+ error(`Script "${name}" not found`);
682
+ return;
683
+ }
684
+
685
+ if (!newName) {
686
+ const { n } = await inquirer.prompt([{
687
+ type: 'input',
688
+ name: 'n',
689
+ message: theme.gold('New name:'),
690
+ validate: v => /^[a-zA-Z0-9_-]+$/.test(v) || 'Alphanumeric, dashes, underscores only',
691
+ }]);
692
+ newName = n;
693
+ }
694
+
695
+ script.name = newName;
696
+ script.createdAt = new Date().toISOString();
697
+ script.lastRun = null;
698
+ script.runCount = 0;
699
+ saveScript(script);
700
+
701
+ success(`Script cloned: ${name} → ${newName}`);
702
+ }
703
+
704
+ // List available templates
705
+ export function listTemplates() {
706
+ showSection('SCRIPT TEMPLATES');
707
+
708
+ const rows = Object.entries(TEMPLATES).map(([key, t]) => [
709
+ theme.gold(key),
710
+ t.name,
711
+ t.description,
712
+ t.params.join(', ') || '(none)',
713
+ ]);
714
+
715
+ table(['Key', 'Name', 'Description', 'Parameters'], rows);
716
+ }
717
+
718
+ export { SCRIPTS_DIR, TEMPLATES };