aether-hub 1.0.3

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,685 @@
1
+ /**
2
+ * aether-cli init
3
+ *
4
+ * Onboarding wizard for new validators.
5
+ * Guides users through identity creation, stake account setup, and testnet connection.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { spawn, execSync } = require('child_process');
11
+ const nacl = require('tweetnacl');
12
+ const bs58 = require('bs58').default;
13
+ const readline = require('readline');
14
+ const os = require('os');
15
+
16
+ // ANSI colors
17
+ const colors = {
18
+ reset: '\x1b[0m',
19
+ bright: '\x1b[1m',
20
+ green: '\x1b[32m',
21
+ yellow: '\x1b[33m',
22
+ cyan: '\x1b[36m',
23
+ red: '\x1b[31m',
24
+ dim: '\x1b[2m',
25
+ magenta: '\x1b[35m',
26
+ };
27
+
28
+ /**
29
+ * Create readline interface for user input
30
+ */
31
+ function createReadline() {
32
+ return readline.createInterface({
33
+ input: process.stdin,
34
+ output: process.stdout,
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Ask a yes/no question
40
+ */
41
+ function askQuestion(rl, question, defaultValue = 'y') {
42
+ return new Promise((resolve, reject) => {
43
+ if (rl.closed) {
44
+ reject(new Error('Readline interface closed'));
45
+ return;
46
+ }
47
+ const suffix = defaultValue === 'y' ? ' [Y/n]' : ' [y/N]';
48
+ rl.question(`${colors.cyan}${question}${suffix}: ${colors.reset}`, (answer) => {
49
+ const normalized = answer.trim().toLowerCase();
50
+ if (normalized === '') {
51
+ resolve(defaultValue === 'y');
52
+ } else {
53
+ resolve(normalized === 'y' || normalized === 'yes');
54
+ }
55
+ });
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Ask for a string value
61
+ */
62
+ function askValue(rl, question, defaultValue = '') {
63
+ return new Promise((resolve) => {
64
+ const suffix = defaultValue ? ` [${defaultValue}]` : '';
65
+ rl.question(`${colors.cyan}${question}${suffix}: ${colors.reset}`, (value) => {
66
+ resolve(value.trim() || defaultValue);
67
+ });
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Print section header
73
+ */
74
+ function printSection(title) {
75
+ console.log();
76
+ console.log(`${colors.bright}${colors.cyan}${'═'.repeat(60)}${colors.reset}`);
77
+ console.log(`${colors.bright}${colors.cyan} ${title}${colors.reset}`);
78
+ console.log(`${colors.bright}${colors.cyan}${'═'.repeat(60)}${colors.reset}`);
79
+ console.log();
80
+ }
81
+
82
+ /**
83
+ * Print a step indicator
84
+ */
85
+ function printStep(step, total, title) {
86
+ console.log();
87
+ console.log(`${colors.yellow}Step ${step}/${total}:${colors.reset} ${colors.bright}${title}${colors.reset}`);
88
+ console.log(`${colors.dim}${'─'.repeat(60)}${colors.reset}`);
89
+ console.log();
90
+ }
91
+
92
+ /**
93
+ * Print success message
94
+ */
95
+ function printSuccess(message) {
96
+ console.log(` ${colors.green}✓${colors.reset} ${message}`);
97
+ }
98
+
99
+ /**
100
+ * Print warning message
101
+ */
102
+ function printWarning(message) {
103
+ console.log(` ${colors.yellow}⚠${colors.reset} ${message}`);
104
+ }
105
+
106
+ /**
107
+ * Print error message
108
+ */
109
+ function printError(message) {
110
+ console.log(` ${colors.red}✗${colors.reset} ${message}`);
111
+ }
112
+
113
+ /**
114
+ * Print the welcome banner
115
+ */
116
+ function printBanner() {
117
+ console.log(`
118
+ ${colors.cyan}╔═══════════════════════════════════════════════════════════════╗
119
+ ${colors.cyan}║ ║
120
+ ${colors.cyan}║ ${colors.bright}███████╗██╗███████╗██╗ ██╗████████╗${colors.reset}${colors.cyan} ║
121
+ ${colors.cyan}║ ${colors.bright}██╔════╝██║██╔════╝╚██╗██╔╝╚══██╔══╝${colors.reset}${colors.cyan} ║
122
+ ${colors.cyan}║ ${colors.bright}███████╗██║███████╗ ╚███╔╝ ██║${colors.reset}${colors.cyan} ║
123
+ ${colors.cyan}║ ${colors.bright}╚════██║██║╚════██║ ██╔██╗ ██║${colors.reset}${colors.cyan} ║
124
+ ${colors.cyan}║ ${colors.bright}███████║██║███████║██╔╝ ██╗ ██║${colors.reset}${colors.cyan} ║
125
+ ${colors.cyan}║ ${colors.bright}╚══════╝╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝${colors.reset}${colors.cyan} ║
126
+ ${colors.cyan}║ ║
127
+ ${colors.cyan}║ ${colors.bright}AETHER VALIDATOR ONBOARDING WIZARD${colors.reset}${colors.cyan} ║
128
+ ${colors.cyan}║ ║
129
+ ${colors.cyan}╚═══════════════════════════════════════════════════════════════╝${colors.reset}
130
+ `);
131
+ }
132
+
133
+ /**
134
+ * Check prerequisites
135
+ */
136
+ async function checkPrerequisites(rl) {
137
+ printStep(1, 4, 'Checking Prerequisites');
138
+
139
+ const checks = [];
140
+
141
+ // Check Node.js
142
+ const nodeVersion = process.version;
143
+ checks.push({
144
+ name: 'Node.js',
145
+ passed: parseInt(nodeVersion.slice(1).split('.')[0]) >= 14,
146
+ message: `Node.js ${nodeVersion}`,
147
+ });
148
+
149
+ // Check Rust
150
+ try {
151
+ const rustVersion = await runCommand('rustc --version');
152
+ checks.push({
153
+ name: 'Rust',
154
+ passed: true,
155
+ message: rustVersion.trim(),
156
+ });
157
+ } catch (e) {
158
+ checks.push({
159
+ name: 'Rust',
160
+ passed: false,
161
+ message: 'Not installed',
162
+ });
163
+ }
164
+
165
+ // Check Cargo
166
+ try {
167
+ await runCommand('cargo --version');
168
+ checks.push({
169
+ name: 'Cargo',
170
+ passed: true,
171
+ message: 'Installed',
172
+ });
173
+ } catch (e) {
174
+ checks.push({
175
+ name: 'Cargo',
176
+ passed: false,
177
+ message: 'Not installed',
178
+ });
179
+ }
180
+
181
+ // Check disk space
182
+ const diskSpace = getDiskSpace();
183
+ checks.push({
184
+ name: 'Disk Space',
185
+ passed: diskSpace.free >= 100,
186
+ message: `${diskSpace.free} GB free`,
187
+ });
188
+
189
+ console.log('Checking system requirements...\n');
190
+
191
+ let allPassed = true;
192
+ for (const check of checks) {
193
+ const status = check.passed ? `${colors.green}✓${colors.reset}` : `${colors.red}✗${colors.reset}`;
194
+ console.log(` ${status} ${check.name}: ${check.message}`);
195
+ if (!check.passed) {
196
+ allPassed = false;
197
+ }
198
+ }
199
+
200
+ if (!allPassed) {
201
+ console.log();
202
+ printWarning('Some prerequisites are missing. Please install them before continuing.');
203
+ const continueAnyway = await askQuestion(rl, 'Continue anyway?', 'n');
204
+ if (!continueAnyway) {
205
+ console.log('\nRun the following to install prerequisites:');
206
+ console.log(' curl --proto \'=https\' --tlsv1.2 -sSf https://sh.rustup.rs | sh');
207
+ console.log(' # Then restart your terminal');
208
+ process.exit(1);
209
+ }
210
+ }
211
+
212
+ return true;
213
+ }
214
+
215
+ /**
216
+ * Find the aether-validator binary
217
+ * Searches in order:
218
+ * 1. JELLY_LEGS_ROOT env var (for CI/automated setups)
219
+ * 2. Current working directory (when running from repo clone)
220
+ * 3. Platform-specific default install locations
221
+ * 4. Sibling repo relative path (when aether-cli is in node_modules)
222
+ * 5. System PATH
223
+ */
224
+ function findValidatorBinary() {
225
+ const platform = os.platform();
226
+ const isWindows = platform === 'win32';
227
+ const binaryName = isWindows ? 'aether-validator.exe' : 'aether-validator';
228
+ const execExt = isWindows ? '.exe' : '';
229
+
230
+ // Helper to check a location and return it if it exists
231
+ const tryPath = (loc) => {
232
+ try {
233
+ if (fs.existsSync(loc)) return { type: 'binary', path: loc };
234
+ } catch {}
235
+ return null;
236
+ };
237
+
238
+ // 1. JELLY_LEGS_ROOT env var
239
+ const jellyRoot = process.env.JELLY_LEGS_ROOT;
240
+ if (jellyRoot) {
241
+ const r = tryPath(path.join(jellyRoot, 'target', 'debug', binaryName))
242
+ || tryPath(path.join(jellyRoot, 'target', 'release', binaryName));
243
+ if (r) return r;
244
+ }
245
+
246
+ // 2. Current working directory
247
+ const cwd = process.cwd();
248
+ const r = tryPath(path.join(cwd, 'target', 'debug', binaryName))
249
+ || tryPath(path.join(cwd, 'target', 'release', binaryName))
250
+ || tryPath(path.join(cwd, binaryName));
251
+ if (r) return r;
252
+
253
+ // 3. Platform-specific locations
254
+ if (isWindows) {
255
+ const r2 = tryPath('C:\\Users\\' + (process.env.USERNAME || 'User') + '\\.jelly-legs\\aether-validator.exe')
256
+ || tryPath('C:\\jelly-legs\\aether-validator.exe');
257
+ if (r2) return r2;
258
+ } else {
259
+ const r2 = tryPath(path.join(os.homedir(), '.jelly-legs', 'aether-validator'))
260
+ || tryPath('/usr/local/bin/aether-validator')
261
+ || tryPath('/usr/bin/aether-validator');
262
+ if (r2) return r2;
263
+ }
264
+
265
+ // 4. Sibling repo path (when aether-cli is inside node_modules of jelly-legs repo)
266
+ const siblingRepo = path.join(__dirname, '..', '..', 'Jelly-legs-unsteady-workshop');
267
+ const r3 = tryPath(path.join(siblingRepo, 'target', 'debug', binaryName))
268
+ || tryPath(path.join(siblingRepo, 'target', 'release', binaryName));
269
+ if (r3) return r3;
270
+
271
+ // 5. Local aether-cli target
272
+ const r4 = tryPath(path.join(__dirname, '..', 'target', 'debug', binaryName))
273
+ || tryPath(path.join(__dirname, '..', 'target', 'release', binaryName));
274
+ if (r4) return r4;
275
+
276
+ // 6. System PATH
277
+ try {
278
+ const checkCmd = isWindows ? 'where' : 'which';
279
+ const out = execSync(`${checkCmd} aether-validator${execExt}`, { stdio: 'pipe' }).toString().trim();
280
+ const firstLine = out.split('\n')[0];
281
+ if (firstLine && fs.existsSync(firstLine)) return { type: 'binary', path: firstLine, inPath: true };
282
+ } catch {}
283
+
284
+ // Not found
285
+ return { type: 'missing', path: null };
286
+ }
287
+
288
+ /**
289
+ * Build the validator binary if missing
290
+ * Returns the found binary path on success, null on failure.
291
+ */
292
+ function buildValidator() {
293
+ // Find the repo root (where Cargo.toml lives)
294
+ const repoPath = path.join(__dirname, '..', '..', 'Jelly-legs-unsteady-workshop');
295
+ const altRepoPath = path.join(__dirname, '..', '..'); // fallback: aether-cli itself
296
+
297
+ // Pick whichever has a Cargo.toml
298
+ const cargoToml = path.join(repoPath, 'Cargo.toml');
299
+ const effectiveRepo = fs.existsSync(cargoToml) ? repoPath : altRepoPath;
300
+
301
+ // Use explicit cargo path on Windows
302
+ const platform = os.platform();
303
+ const isWindows = platform === 'win32';
304
+ let cargoCmd = 'cargo';
305
+ if (isWindows) {
306
+ const explicitCargo = 'C:\\Users\\RM_Ga\\.cargo\\bin\\cargo.exe';
307
+ if (fs.existsSync(explicitCargo)) cargoCmd = explicitCargo;
308
+ }
309
+
310
+ console.log(` ${colors.cyan}Building aether-validator...${colors.reset}`);
311
+ if (isWindows) console.log(` (using ${cargoCmd})`);
312
+
313
+ try {
314
+ execSync(`${cargoCmd} build --bin aether-validator`, {
315
+ cwd: effectiveRepo,
316
+ stdio: 'inherit',
317
+ shell: false, // don't rely on shell for path lookup
318
+ });
319
+
320
+ // Re-check for binary
321
+ const result = findValidatorBinary();
322
+ if (result.type === 'binary') {
323
+ console.log(` ${colors.green}✓ Build successful!${colors.reset}`);
324
+ return result;
325
+ }
326
+
327
+ console.error(` ${colors.red}✗ Build completed but binary not found${colors.reset}`);
328
+ return null;
329
+ } catch (err) {
330
+ // Fallback: try once more with shell enabled (handles PATH issues on some systems)
331
+ try {
332
+ console.log(` ${colors.yellow}Retry with shell fallback...${colors.reset}`);
333
+ execSync(`${cargoCmd} build --bin aether-validator`, {
334
+ cwd: effectiveRepo,
335
+ stdio: 'inherit',
336
+ shell: true,
337
+ });
338
+ const result = findValidatorBinary();
339
+ if (result.type === 'binary') {
340
+ console.log(` ${colors.green}✓ Build successful!${colors.reset}`);
341
+ return result;
342
+ }
343
+ } catch {}
344
+ console.error(` ${colors.red}✗ Build failed: ${err.message}${colors.reset}`);
345
+ return null;
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Run the validator binary with args
351
+ * Handles missing binary by offering to build it
352
+ */
353
+ function runValidatorBinary(args, options = {}, rl = null) {
354
+ let result = findValidatorBinary();
355
+
356
+ // Handle missing binary
357
+ if (result.type === 'missing') {
358
+ console.log(` ${colors.yellow}⚠ Validator binary not found${colors.reset}`);
359
+ console.log(` ${colors.cyan}Would you like to build it now? (cargo build --bin aether-validator)${colors.reset}`);
360
+ console.log();
361
+
362
+ if (rl) {
363
+ return new Promise((resolve, reject) => {
364
+ rl.question(' Build now? [Y/n] ', (answer) => {
365
+ if (answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') {
366
+ reject(new Error('User declined to build validator'));
367
+ return;
368
+ }
369
+
370
+ const built = buildValidator();
371
+ if (!built) {
372
+ reject(new Error('Build failed'));
373
+ return;
374
+ }
375
+ result = built;
376
+ resolve(runValidatorBinaryInternal(result, args, options));
377
+ });
378
+ });
379
+ } else {
380
+ // No readline available - try to build automatically
381
+ const built = buildValidator();
382
+ if (!built) {
383
+ throw new Error('Validator binary not found and build failed');
384
+ }
385
+ result = built;
386
+ }
387
+ }
388
+
389
+ return runValidatorBinaryInternal(result, args, options);
390
+ }
391
+
392
+ /**
393
+ * Internal function to run the validator binary (assumes binary exists)
394
+ */
395
+ function runValidatorBinaryInternal({ type, path: binaryPath, inPath }, args, options = {}) {
396
+ const workspaceRoot = path.join(__dirname, '..', '..');
397
+ const repoPath = path.join(workspaceRoot, 'Jelly-legs-unsteady-workshop');
398
+
399
+ const result = execSync(`"${binaryPath}" ${args.join(' ')}`, {
400
+ cwd: inPath ? undefined : repoPath,
401
+ stdio: 'inherit',
402
+ shell: true,
403
+ ...options,
404
+ });
405
+
406
+ return { type, path: binaryPath, inPath };
407
+ }
408
+
409
+ /**
410
+ * Generate a new validator Ed25519 keypair using TweetNaCl (pure JS, no Rust needed)
411
+ * Compatible with the Rust keypair format:
412
+ * Rust SigningKey::from_bytes expects 32-byte seed
413
+ * TweetNaCl secretKey is 64 bytes: seed(32) || publicKey(32)
414
+ * We encode only the 32-byte seed as bs58 to match the Rust format
415
+ */
416
+ function generateEd25519Identity(outPath, force = false) {
417
+ const identityFile = outPath || path.join(process.cwd(), 'validator-identity.json');
418
+
419
+ if (fs.existsSync(identityFile) && !force) {
420
+ throw new Error(`Identity file already exists at ${identityFile}. Use --force to overwrite.`);
421
+ }
422
+
423
+ // Generate a new Ed25519 keypair using TweetNaCl
424
+ const keyPair = nacl.sign.keyPair();
425
+
426
+ // TweetNaCl secretKey is 64 bytes: seed(32) || publicKey(32)
427
+ // Rust SigningKey::from_bytes expects the 32-byte seed only
428
+ const seed32 = keyPair.secretKey.slice(0, 32);
429
+
430
+ const secretBs58 = bs58.encode(seed32);
431
+ const publicBs58 = bs58.encode(keyPair.publicKey);
432
+
433
+ const identity = {
434
+ pubkey: publicBs58,
435
+ secret: secretBs58,
436
+ };
437
+
438
+ fs.writeFileSync(identityFile, JSON.stringify(identity, null, 2));
439
+ return { identityFile, publicKey: publicBs58 };
440
+ }
441
+
442
+ async function generateIdentity(rl, tier = 'full') {
443
+ printStep(3, 5, 'Generating Validator Identity');
444
+
445
+ const identityPath = path.join(process.cwd(), 'validator-identity.json');
446
+
447
+ // Check if identity already exists
448
+ if (fs.existsSync(identityPath)) {
449
+ printWarning('Validator identity already exists at validator-identity.json');
450
+ try {
451
+ const regenerate = await askQuestion(rl, 'Regenerate identity?', 'n');
452
+ if (!regenerate) {
453
+ printSuccess('Using existing identity');
454
+ return identityPath;
455
+ }
456
+ } catch (err) {
457
+ // Readline may have closed - skip regeneration prompt
458
+ printSuccess('Using existing identity');
459
+ return identityPath;
460
+ }
461
+ }
462
+
463
+ console.log('\nGenerating new Ed25519 keypair (pure JS)...');
464
+ console.log(`${colors.dim}Tier: ${tier.toUpperCase()}${colors.reset}`);
465
+
466
+ try {
467
+ const { identityFile, publicKey } = generateEd25519Identity(identityPath, true);
468
+ printSuccess(`Identity saved to ${path.basename(identityFile)}`);
469
+ console.log(` ${colors.dim}Public key: ${publicKey}${colors.reset}`);
470
+ } catch (e) {
471
+ printError(`Failed to create identity: ${e.message}`);
472
+ printWarning('You can create it manually with: node -e "const nacl=require(\'tweetnacl\');..."');
473
+ process.exit(1);
474
+ }
475
+
476
+ console.log();
477
+ printWarning('IMPORTANT: Backup your validator-identity.json file!');
478
+ printWarning('If you lose this file, you lose your validator status.');
479
+
480
+ return identityPath;
481
+ }
482
+
483
+ /**
484
+ * Connect to testnet
485
+ */
486
+ async function connectTestnet(rl, tier = 'full') {
487
+ printStep(4, 5, 'Connecting to Testnet');
488
+
489
+ console.log('The validator will connect to the AETHER testnet.');
490
+ console.log('Testnet uses aether-testnet-1 chain ID with reduced stake requirements.\n');
491
+ console.log(`${colors.dim}Selected tier: ${tier.toUpperCase()}${colors.reset}`);
492
+ console.log();
493
+
494
+ try {
495
+ const startNow = await askQuestion(rl, 'Start validator now?', 'y');
496
+
497
+ if (startNow) {
498
+ console.log('\nStarting validator in testnet mode...\n');
499
+
500
+ // Close readline before handing off to validator (it manages its own stdin)
501
+ rl.close();
502
+
503
+ const validatorStart = require('./validator-start');
504
+ validatorStart.validatorStart(tier);
505
+ return true;
506
+ }
507
+ } catch (err) {
508
+ // Readline closed or error - skip the prompt
509
+ console.log(` ${colors.yellow}⚠ Skipping prompt (interactive mode unavailable)${colors.reset}`);
510
+ }
511
+
512
+ console.log();
513
+ printSuccess('You can start the validator later with:');
514
+ console.log(` ${colors.bright}aether-cli validator start --testnet --tier ${tier}${colors.reset}`);
515
+
516
+ return true;
517
+ }
518
+
519
+ /**
520
+ * Print completion summary
521
+ */
522
+ async function printSummary(tierBadge = '[FULL]') {
523
+ printStep(5, 5, 'Setup Complete');
524
+
525
+ console.log(`
526
+ ${colors.green}╔═══════════════════════════════════════════════════════════════╗
527
+ ${colors.green}║ ║
528
+ ${colors.green}║ ${colors.bright}✅ VALIDATOR SETUP COMPLETE${colors.reset}${colors.green} ║
529
+ ${colors.green}║ ${colors.dim}Tier: ${tierBadge}${colors.reset}${colors.green} ║
530
+ ${colors.green}║ ║
531
+ ${colors.green}╚═══════════════════════════════════════════════════════════════╝${colors.reset}
532
+ `);
533
+
534
+ console.log('Useful commands:');
535
+ console.log(` ${colors.bright}aether-cli validator status${colors.reset} Check validator status`);
536
+ console.log(` ${colors.bright}aether-cli validator start${colors.reset} Start the validator`);
537
+ console.log(` ${colors.bright}aether-cli doctor${colors.reset} Run system checks`);
538
+ console.log();
539
+
540
+ console.log('Next steps:');
541
+ console.log(' 1. Fund your validator wallet with testnet AETH');
542
+ console.log(' 2. Create a stake account: aether-validator create-stake-account');
543
+ console.log(' 3. Monitor your validator: aether-cli validator status');
544
+ console.log();
545
+ }
546
+
547
+ /**
548
+ * Run an external command
549
+ */
550
+ function runCommand(cmd, timeout = 30000) {
551
+ return new Promise((resolve, reject) => {
552
+ const child = spawn(cmd, [], {
553
+ shell: true,
554
+ stdio: 'pipe',
555
+ });
556
+
557
+ let stdout = '';
558
+ let stderr = '';
559
+
560
+ child.stdout.on('data', (data) => { stdout += data.toString(); });
561
+ child.stderr.on('data', (data) => { stderr += data.toString(); });
562
+
563
+ const timer = setTimeout(() => {
564
+ child.kill();
565
+ reject(new Error('Command timed out'));
566
+ }, timeout);
567
+
568
+ child.on('close', (code) => {
569
+ clearTimeout(timer);
570
+ if (code === 0) {
571
+ resolve(stdout);
572
+ } else {
573
+ reject(new Error(stderr || `Command exited with code ${code}`));
574
+ }
575
+ });
576
+
577
+ child.on('error', reject);
578
+ });
579
+ }
580
+
581
+ /**
582
+ * Get approximate disk space (cross-platform)
583
+ */
584
+ function getDiskSpace() {
585
+ try {
586
+ if (process.platform === 'win32') {
587
+ const { execSync } = require('child_process');
588
+ const output = execSync('powershell -c "(Get-PSDrive -Name C).Free / 1GB"', { encoding: 'utf8' });
589
+ return { free: parseFloat(output.trim()) };
590
+ } else {
591
+ const output = fs.readFileSync('/dev/null', 'utf8');
592
+ const stat = fs.statfsSync('/');
593
+ return { free: stat.bsize * stat.bfree / (1024 * 1024 * 1024) };
594
+ }
595
+ } catch (e) {
596
+ return { free: 100 }; // Assume sufficient if can't check
597
+ }
598
+ }
599
+
600
+ /**
601
+ * Select validator tier with hardware summary
602
+ */
603
+ async function selectTier(rl) {
604
+ printStep(2, 5, 'Select Validator Tier');
605
+
606
+ console.log('Choose your validator tier based on your hardware and stake:');
607
+ console.log();
608
+ console.log(` ${colors.bright}[1] FULL${colors.reset} - 10K AETH stake, 8 cores, 32GB RAM, 512GB SSD`);
609
+ console.log(` Full consensus weight (1.0x), block production, voting rights`);
610
+ console.log();
611
+ console.log(` ${colors.bright}[2] LITE${colors.reset} - 1K AETH stake, 4 cores, 8GB RAM, 100GB SSD`);
612
+ console.log(` Stake-based weight (stake/10000), voting only, no solo blocks`);
613
+ console.log();
614
+ console.log(` ${colors.bright}[3] OBSERVER${colors.reset} - 0 AETH stake, 2 cores, 4GB RAM, 50GB disk`);
615
+ console.log(` Relay-only, earns FLUX via data relay, no voting rights`);
616
+ console.log();
617
+
618
+ const tierChoice = await askValue(rl, 'Select tier [1/2/3]', '1');
619
+
620
+ let selectedTier = 'full';
621
+ let tierBadge = '[FULL]';
622
+
623
+ switch (tierChoice.trim()) {
624
+ case '1':
625
+ selectedTier = 'full';
626
+ tierBadge = '[FULL]';
627
+ console.log(`\n ${colors.green}✓ Selected: FULL Validator${colors.reset}`);
628
+ console.log(` ${colors.dim}Hardware: 8+ cores, 32GB+ RAM, 512GB+ SSD, 100Mbps+${colors.reset}`);
629
+ console.log(` ${colors.dim}Stake: 10,000 AETH minimum${colors.reset}`);
630
+ console.log(` ${colors.dim}Consensus: 1.0x weight, full voting + block production${colors.reset}`);
631
+ break;
632
+ case '2':
633
+ selectedTier = 'lite';
634
+ tierBadge = '[LITE]';
635
+ console.log(`\n ${colors.green}✓ Selected: LITE Validator${colors.reset}`);
636
+ console.log(` ${colors.dim}Hardware: 4+ cores, 8GB+ RAM, 100GB+ SSD, 25Mbps+${colors.reset}`);
637
+ console.log(` ${colors.dim}Stake: 1,000 AETH minimum${colors.reset}`);
638
+ console.log(` ${colors.dim}Consensus: stake/10000 weight, voting only${colors.reset}`);
639
+ break;
640
+ case '3':
641
+ selectedTier = 'observer';
642
+ tierBadge = '[OBSERVER]';
643
+ console.log(`\n ${colors.green}✓ Selected: OBSERVER Node${colors.reset}`);
644
+ console.log(` ${colors.dim}Hardware: 2+ cores, 4GB+ RAM, 50GB+ disk, 10Mbps+${colors.reset}`);
645
+ console.log(` ${colors.dim}Stake: 0 AETH (relay-only)${colors.reset}`);
646
+ console.log(` ${colors.dim}Earnings: FLUX via data relay (0.000001 FLUX/byte)${colors.reset}`);
647
+ break;
648
+ default:
649
+ selectedTier = 'full';
650
+ tierBadge = '[FULL]';
651
+ console.log(`\n ${colors.yellow}⚠ Invalid choice, defaulting to FULL${colors.reset}`);
652
+ }
653
+
654
+ console.log();
655
+ printWarning('You can change tiers later by re-running init with a different selection.');
656
+
657
+ return { tier: selectedTier, badge: tierBadge };
658
+ }
659
+
660
+ /**
661
+ * Main init command
662
+ */
663
+ async function init() {
664
+ printBanner();
665
+
666
+ const rl = createReadline();
667
+
668
+ try {
669
+ await checkPrerequisites(rl);
670
+ const { tier, badge } = await selectTier(rl);
671
+ await generateIdentity(rl, tier);
672
+ await connectTestnet(rl, tier);
673
+ await printSummary(badge);
674
+ } finally {
675
+ rl.close();
676
+ }
677
+ }
678
+
679
+ // Export for use as module
680
+ module.exports = { init, findValidatorBinary, buildValidator, runValidatorBinary };
681
+
682
+ // Run if called directly
683
+ if (require.main === module) {
684
+ init();
685
+ }