@ruvector/edge-net 0.1.0 → 0.1.2

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/networks.js ADDED
@@ -0,0 +1,817 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Edge-Net Multi-Network Module
4
+ *
5
+ * Enables creation, discovery, and contribution to multiple edge networks.
6
+ * Each network is cryptographically isolated with its own:
7
+ * - Genesis block and network ID
8
+ * - QDAG ledger
9
+ * - Peer registry
10
+ * - Access control (public/private/invite-only)
11
+ *
12
+ * Security Features:
13
+ * - Network ID derived from genesis hash (tamper-evident)
14
+ * - Ed25519 signatures for network announcements
15
+ * - Optional invite codes for private networks
16
+ * - Cryptographic proof of network membership
17
+ */
18
+
19
+ import { createHash, randomBytes } from 'crypto';
20
+ import { promises as fs } from 'fs';
21
+ import { homedir } from 'os';
22
+ import { join, dirname } from 'path';
23
+ import { fileURLToPath } from 'url';
24
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
25
+
26
+ const __filename = fileURLToPath(import.meta.url);
27
+ const __dirname = dirname(__filename);
28
+
29
+ // ANSI colors
30
+ const colors = {
31
+ reset: '\x1b[0m',
32
+ bold: '\x1b[1m',
33
+ dim: '\x1b[2m',
34
+ cyan: '\x1b[36m',
35
+ green: '\x1b[32m',
36
+ yellow: '\x1b[33m',
37
+ blue: '\x1b[34m',
38
+ magenta: '\x1b[35m',
39
+ red: '\x1b[31m',
40
+ };
41
+
42
+ const c = (color, text) => `${colors[color]}${text}${colors.reset}`;
43
+
44
+ // Network types
45
+ const NetworkType = {
46
+ PUBLIC: 'public', // Anyone can join and discover
47
+ PRIVATE: 'private', // Requires invite code to join
48
+ CONSORTIUM: 'consortium', // Requires approval from existing members
49
+ };
50
+
51
+ // Well-known public networks (bootstrap)
52
+ const WELL_KNOWN_NETWORKS = [
53
+ {
54
+ id: 'mainnet',
55
+ name: 'Edge-Net Mainnet',
56
+ description: 'Primary public compute network',
57
+ type: NetworkType.PUBLIC,
58
+ genesisHash: 'edgenet-mainnet-genesis-v1',
59
+ bootstrapNodes: ['edge-net.ruvector.dev:9000'],
60
+ created: '2024-01-01T00:00:00Z',
61
+ },
62
+ {
63
+ id: 'testnet',
64
+ name: 'Edge-Net Testnet',
65
+ description: 'Testing and development network',
66
+ type: NetworkType.PUBLIC,
67
+ genesisHash: 'edgenet-testnet-genesis-v1',
68
+ bootstrapNodes: ['testnet.ruvector.dev:9000'],
69
+ created: '2024-01-01T00:00:00Z',
70
+ },
71
+ ];
72
+
73
+ // Directory structure
74
+ function getNetworksDir() {
75
+ const dir = join(homedir(), '.ruvector', 'networks');
76
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
77
+ return dir;
78
+ }
79
+
80
+ function getRegistryFile() {
81
+ return join(getNetworksDir(), 'registry.json');
82
+ }
83
+
84
+ function getNetworkDir(networkId) {
85
+ const dir = join(getNetworksDir(), networkId);
86
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
87
+ return dir;
88
+ }
89
+
90
+ /**
91
+ * Network Genesis - defines a network's identity
92
+ */
93
+ export class NetworkGenesis {
94
+ constructor(options = {}) {
95
+ this.version = 1;
96
+ this.name = options.name || 'Custom Network';
97
+ this.description = options.description || 'A custom edge-net network';
98
+ this.type = options.type || NetworkType.PUBLIC;
99
+ this.creator = options.creator || null; // Creator's public key
100
+ this.creatorSiteId = options.creatorSiteId || 'anonymous';
101
+ this.created = options.created || new Date().toISOString();
102
+ this.parameters = {
103
+ minContributors: options.minContributors || 1,
104
+ confirmationThreshold: options.confirmationThreshold || 3,
105
+ creditMultiplier: options.creditMultiplier || 1.0,
106
+ maxPeers: options.maxPeers || 100,
107
+ ...options.parameters,
108
+ };
109
+ this.inviteRequired = this.type !== NetworkType.PUBLIC;
110
+ this.approvers = options.approvers || []; // For consortium networks
111
+ this.nonce = options.nonce || randomBytes(16).toString('hex');
112
+ }
113
+
114
+ /**
115
+ * Compute network ID from genesis hash
116
+ */
117
+ computeNetworkId() {
118
+ const data = JSON.stringify({
119
+ version: this.version,
120
+ name: this.name,
121
+ type: this.type,
122
+ creator: this.creator,
123
+ created: this.created,
124
+ parameters: this.parameters,
125
+ nonce: this.nonce,
126
+ });
127
+
128
+ const hash = createHash('sha256').update(data).digest('hex');
129
+ return `net-${hash.slice(0, 16)}`;
130
+ }
131
+
132
+ /**
133
+ * Create signed genesis block
134
+ */
135
+ createSignedGenesis(signFn) {
136
+ const genesis = {
137
+ ...this,
138
+ networkId: this.computeNetworkId(),
139
+ };
140
+
141
+ if (signFn) {
142
+ const dataToSign = JSON.stringify(genesis);
143
+ genesis.signature = signFn(dataToSign);
144
+ }
145
+
146
+ return genesis;
147
+ }
148
+
149
+ /**
150
+ * Generate invite code for private networks
151
+ */
152
+ generateInviteCode() {
153
+ if (this.type === NetworkType.PUBLIC) {
154
+ throw new Error('Public networks do not require invite codes');
155
+ }
156
+
157
+ const networkId = this.computeNetworkId();
158
+ const secret = randomBytes(16).toString('hex');
159
+ const code = Buffer.from(`${networkId}:${secret}`).toString('base64url');
160
+
161
+ return {
162
+ code,
163
+ networkId,
164
+ validUntil: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days
165
+ };
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Network Registry - manages known networks
171
+ */
172
+ export class NetworkRegistry {
173
+ constructor() {
174
+ this.networks = new Map();
175
+ this.activeNetwork = null;
176
+ this.loaded = false;
177
+ }
178
+
179
+ async load() {
180
+ try {
181
+ // Load well-known networks
182
+ for (const network of WELL_KNOWN_NETWORKS) {
183
+ this.networks.set(network.id, {
184
+ ...network,
185
+ isWellKnown: true,
186
+ joined: false,
187
+ stats: null,
188
+ });
189
+ }
190
+
191
+ // Load user's network registry
192
+ if (existsSync(getRegistryFile())) {
193
+ const data = JSON.parse(await fs.readFile(getRegistryFile(), 'utf-8'));
194
+
195
+ for (const network of data.networks || []) {
196
+ this.networks.set(network.id, {
197
+ ...network,
198
+ isWellKnown: false,
199
+ });
200
+ }
201
+
202
+ this.activeNetwork = data.activeNetwork || null;
203
+ }
204
+
205
+ this.loaded = true;
206
+ } catch (err) {
207
+ console.error('Failed to load network registry:', err.message);
208
+ }
209
+ }
210
+
211
+ async save() {
212
+ const data = {
213
+ version: 1,
214
+ activeNetwork: this.activeNetwork,
215
+ networks: Array.from(this.networks.values()).filter(n => !n.isWellKnown),
216
+ savedAt: new Date().toISOString(),
217
+ };
218
+
219
+ await fs.writeFile(getRegistryFile(), JSON.stringify(data, null, 2));
220
+ }
221
+
222
+ /**
223
+ * Create a new network
224
+ */
225
+ async createNetwork(options, identity) {
226
+ const genesis = new NetworkGenesis({
227
+ ...options,
228
+ creator: identity?.publicKey,
229
+ creatorSiteId: identity?.siteId,
230
+ });
231
+
232
+ const networkId = genesis.computeNetworkId();
233
+
234
+ // Create network directory structure
235
+ const networkDir = getNetworkDir(networkId);
236
+ await fs.mkdir(join(networkDir, 'peers'), { recursive: true });
237
+
238
+ // Save genesis block
239
+ const genesisData = genesis.createSignedGenesis(
240
+ identity?.sign ? (data) => identity.sign(data) : null
241
+ );
242
+ await fs.writeFile(
243
+ join(networkDir, 'genesis.json'),
244
+ JSON.stringify(genesisData, null, 2)
245
+ );
246
+
247
+ // Initialize QDAG for this network
248
+ const qdag = {
249
+ networkId,
250
+ nodes: [{
251
+ id: 'genesis',
252
+ type: 'genesis',
253
+ timestamp: Date.now(),
254
+ message: `Genesis: ${genesis.name}`,
255
+ parents: [],
256
+ weight: 1,
257
+ confirmations: 0,
258
+ }],
259
+ tips: ['genesis'],
260
+ confirmed: ['genesis'],
261
+ createdAt: Date.now(),
262
+ };
263
+ await fs.writeFile(
264
+ join(networkDir, 'qdag.json'),
265
+ JSON.stringify(qdag, null, 2)
266
+ );
267
+
268
+ // Initialize peer list
269
+ await fs.writeFile(
270
+ join(networkDir, 'peers.json'),
271
+ JSON.stringify([], null, 2)
272
+ );
273
+
274
+ // Register network
275
+ const networkEntry = {
276
+ id: networkId,
277
+ name: genesis.name,
278
+ description: genesis.description,
279
+ type: genesis.type,
280
+ creator: genesis.creator,
281
+ creatorSiteId: genesis.creatorSiteId,
282
+ created: genesis.created,
283
+ parameters: genesis.parameters,
284
+ genesisHash: createHash('sha256')
285
+ .update(JSON.stringify(genesisData))
286
+ .digest('hex')
287
+ .slice(0, 32),
288
+ joined: true,
289
+ isOwner: true,
290
+ stats: { nodes: 1, contributors: 0, credits: 0 },
291
+ };
292
+
293
+ this.networks.set(networkId, networkEntry);
294
+ await this.save();
295
+
296
+ // Generate invite codes if private
297
+ let inviteCodes = null;
298
+ if (genesis.type !== NetworkType.PUBLIC) {
299
+ inviteCodes = [];
300
+ for (let i = 0; i < 5; i++) {
301
+ inviteCodes.push(genesis.generateInviteCode());
302
+ }
303
+ await fs.writeFile(
304
+ join(networkDir, 'invites.json'),
305
+ JSON.stringify(inviteCodes, null, 2)
306
+ );
307
+ }
308
+
309
+ return { networkId, genesis: genesisData, inviteCodes };
310
+ }
311
+
312
+ /**
313
+ * Join an existing network
314
+ */
315
+ async joinNetwork(networkId, inviteCode = null) {
316
+ const network = this.networks.get(networkId);
317
+
318
+ if (!network) {
319
+ throw new Error(`Network not found: ${networkId}`);
320
+ }
321
+
322
+ if (network.joined) {
323
+ return { alreadyJoined: true, network };
324
+ }
325
+
326
+ // Verify invite code for private networks
327
+ if (network.type === NetworkType.PRIVATE) {
328
+ if (!inviteCode) {
329
+ throw new Error('Private network requires invite code');
330
+ }
331
+
332
+ const isValid = await this.verifyInviteCode(networkId, inviteCode);
333
+ if (!isValid) {
334
+ throw new Error('Invalid or expired invite code');
335
+ }
336
+ }
337
+
338
+ // Create local network directory
339
+ const networkDir = getNetworkDir(networkId);
340
+
341
+ // For well-known networks, create initial structure
342
+ if (network.isWellKnown) {
343
+ const qdag = {
344
+ networkId,
345
+ nodes: [{
346
+ id: 'genesis',
347
+ type: 'genesis',
348
+ timestamp: Date.now(),
349
+ message: `Joined: ${network.name}`,
350
+ parents: [],
351
+ weight: 1,
352
+ confirmations: 0,
353
+ }],
354
+ tips: ['genesis'],
355
+ confirmed: ['genesis'],
356
+ createdAt: Date.now(),
357
+ };
358
+ await fs.writeFile(
359
+ join(networkDir, 'qdag.json'),
360
+ JSON.stringify(qdag, null, 2)
361
+ );
362
+
363
+ await fs.writeFile(
364
+ join(networkDir, 'peers.json'),
365
+ JSON.stringify([], null, 2)
366
+ );
367
+ }
368
+
369
+ network.joined = true;
370
+ network.joinedAt = new Date().toISOString();
371
+ await this.save();
372
+
373
+ return { joined: true, network };
374
+ }
375
+
376
+ /**
377
+ * Verify invite code
378
+ */
379
+ async verifyInviteCode(networkId, code) {
380
+ try {
381
+ const decoded = Buffer.from(code, 'base64url').toString();
382
+ const [codeNetworkId, secret] = decoded.split(':');
383
+
384
+ if (codeNetworkId !== networkId) {
385
+ return false;
386
+ }
387
+
388
+ // In production, verify against network's invite registry
389
+ // For local simulation, accept any properly formatted code
390
+ return secret && secret.length === 32;
391
+ } catch {
392
+ return false;
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Discover networks from DHT/registry
398
+ */
399
+ async discoverNetworks(options = {}) {
400
+ const discovered = [];
401
+
402
+ // Always include well-known networks
403
+ for (const network of WELL_KNOWN_NETWORKS) {
404
+ const existing = this.networks.get(network.id);
405
+ discovered.push({
406
+ ...network,
407
+ joined: existing?.joined || false,
408
+ source: 'well-known',
409
+ });
410
+ }
411
+
412
+ // Scan for locally known networks
413
+ try {
414
+ const networksDir = getNetworksDir();
415
+ const dirs = await fs.readdir(networksDir);
416
+
417
+ for (const dir of dirs) {
418
+ if (dir === 'registry.json') continue;
419
+
420
+ const genesisPath = join(networksDir, dir, 'genesis.json');
421
+ if (existsSync(genesisPath)) {
422
+ try {
423
+ const genesis = JSON.parse(await fs.readFile(genesisPath, 'utf-8'));
424
+ const existing = this.networks.get(genesis.networkId || dir);
425
+
426
+ if (!existing?.isWellKnown) {
427
+ discovered.push({
428
+ id: genesis.networkId || dir,
429
+ name: genesis.name,
430
+ description: genesis.description,
431
+ type: genesis.type,
432
+ creator: genesis.creatorSiteId,
433
+ created: genesis.created,
434
+ joined: existing?.joined || false,
435
+ source: 'local',
436
+ });
437
+ }
438
+ } catch (e) {
439
+ // Skip invalid genesis files
440
+ }
441
+ }
442
+ }
443
+ } catch (err) {
444
+ // Networks directory doesn't exist yet
445
+ }
446
+
447
+ // In production: Query DHT/bootstrap nodes for public networks
448
+ // This is simulated here
449
+
450
+ return discovered;
451
+ }
452
+
453
+ /**
454
+ * Set active network for contributions
455
+ */
456
+ async setActiveNetwork(networkId) {
457
+ const network = this.networks.get(networkId);
458
+
459
+ if (!network) {
460
+ throw new Error(`Network not found: ${networkId}`);
461
+ }
462
+
463
+ if (!network.joined) {
464
+ throw new Error(`Must join network first: ${networkId}`);
465
+ }
466
+
467
+ this.activeNetwork = networkId;
468
+ await this.save();
469
+
470
+ return network;
471
+ }
472
+
473
+ /**
474
+ * Get network info
475
+ */
476
+ getNetwork(networkId) {
477
+ return this.networks.get(networkId);
478
+ }
479
+
480
+ /**
481
+ * Get active network
482
+ */
483
+ getActiveNetwork() {
484
+ if (!this.activeNetwork) return null;
485
+ return this.networks.get(this.activeNetwork);
486
+ }
487
+
488
+ /**
489
+ * Get all joined networks
490
+ */
491
+ getJoinedNetworks() {
492
+ return Array.from(this.networks.values()).filter(n => n.joined);
493
+ }
494
+
495
+ /**
496
+ * Get network statistics
497
+ */
498
+ async getNetworkStats(networkId) {
499
+ const networkDir = getNetworkDir(networkId);
500
+ const qdagPath = join(networkDir, 'qdag.json');
501
+ const peersPath = join(networkDir, 'peers.json');
502
+
503
+ const stats = {
504
+ nodes: 0,
505
+ contributions: 0,
506
+ contributors: 0,
507
+ credits: 0,
508
+ peers: 0,
509
+ };
510
+
511
+ try {
512
+ if (existsSync(qdagPath)) {
513
+ const qdag = JSON.parse(await fs.readFile(qdagPath, 'utf-8'));
514
+ const contributions = (qdag.nodes || []).filter(n => n.type === 'contribution');
515
+
516
+ stats.nodes = qdag.nodes?.length || 0;
517
+ stats.contributions = contributions.length;
518
+ stats.contributors = new Set(contributions.map(c => c.contributor)).size;
519
+ stats.credits = contributions.reduce((sum, c) => sum + (c.credits || 0), 0);
520
+ }
521
+
522
+ if (existsSync(peersPath)) {
523
+ const peers = JSON.parse(await fs.readFile(peersPath, 'utf-8'));
524
+ stats.peers = peers.length;
525
+ }
526
+ } catch (err) {
527
+ // Stats not available
528
+ }
529
+
530
+ return stats;
531
+ }
532
+
533
+ /**
534
+ * List all networks
535
+ */
536
+ listNetworks() {
537
+ return Array.from(this.networks.values());
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Multi-Network Manager - coordinates contributions across networks
543
+ */
544
+ export class MultiNetworkManager {
545
+ constructor(identity) {
546
+ this.identity = identity;
547
+ this.registry = new NetworkRegistry();
548
+ this.activeConnections = new Map();
549
+ }
550
+
551
+ async initialize() {
552
+ await this.registry.load();
553
+ return this;
554
+ }
555
+
556
+ /**
557
+ * Create a new network
558
+ */
559
+ async createNetwork(options) {
560
+ console.log(`\n${c('cyan', 'Creating new network...')}\n`);
561
+
562
+ const result = await this.registry.createNetwork(options, this.identity);
563
+
564
+ console.log(`${c('green', '✓')} Network created successfully!`);
565
+ console.log(` ${c('cyan', 'Network ID:')} ${result.networkId}`);
566
+ console.log(` ${c('cyan', 'Name:')} ${options.name}`);
567
+ console.log(` ${c('cyan', 'Type:')} ${options.type}`);
568
+ console.log(` ${c('cyan', 'Description:')} ${options.description || 'N/A'}`);
569
+
570
+ if (result.inviteCodes) {
571
+ console.log(`\n${c('bold', 'Invite Codes (share these to invite members):')}`);
572
+ for (const invite of result.inviteCodes.slice(0, 3)) {
573
+ console.log(` ${c('yellow', invite.code)}`);
574
+ }
575
+ console.log(` ${c('dim', `(${result.inviteCodes.length} codes saved to network directory)`)}`);
576
+ }
577
+
578
+ console.log(`\n${c('dim', 'Network directory:')} ~/.ruvector/networks/${result.networkId}`);
579
+
580
+ return result;
581
+ }
582
+
583
+ /**
584
+ * Discover available networks
585
+ */
586
+ async discoverNetworks() {
587
+ console.log(`\n${c('cyan', 'Discovering networks...')}\n`);
588
+
589
+ const networks = await this.registry.discoverNetworks();
590
+
591
+ if (networks.length === 0) {
592
+ console.log(` ${c('dim', 'No networks found.')}`);
593
+ return networks;
594
+ }
595
+
596
+ console.log(`${c('bold', 'Available Networks:')}\n`);
597
+
598
+ for (const network of networks) {
599
+ const status = network.joined ? c('green', '● Joined') : c('dim', '○ Not joined');
600
+ const typeIcon = network.type === NetworkType.PUBLIC ? '🌐' :
601
+ network.type === NetworkType.PRIVATE ? '🔒' : '🏢';
602
+
603
+ console.log(` ${status} ${typeIcon} ${c('bold', network.name)}`);
604
+ console.log(` ${c('dim', 'ID:')} ${network.id}`);
605
+ console.log(` ${c('dim', 'Type:')} ${network.type}`);
606
+ console.log(` ${c('dim', 'Description:')} ${network.description || 'N/A'}`);
607
+ console.log(` ${c('dim', 'Source:')} ${network.source}`);
608
+ console.log('');
609
+ }
610
+
611
+ return networks;
612
+ }
613
+
614
+ /**
615
+ * Join a network
616
+ */
617
+ async joinNetwork(networkId, inviteCode = null) {
618
+ console.log(`\n${c('cyan', `Joining network ${networkId}...`)}\n`);
619
+
620
+ try {
621
+ const result = await this.registry.joinNetwork(networkId, inviteCode);
622
+
623
+ if (result.alreadyJoined) {
624
+ console.log(`${c('yellow', '⚠')} Already joined network: ${result.network.name}`);
625
+ } else {
626
+ console.log(`${c('green', '✓')} Successfully joined: ${result.network.name}`);
627
+ }
628
+
629
+ // Set as active if it's the only joined network
630
+ const joinedNetworks = this.registry.getJoinedNetworks();
631
+ if (joinedNetworks.length === 1) {
632
+ await this.registry.setActiveNetwork(networkId);
633
+ console.log(` ${c('dim', 'Set as active network')}`);
634
+ }
635
+
636
+ return result;
637
+ } catch (err) {
638
+ console.log(`${c('red', '✗')} Failed to join: ${err.message}`);
639
+ throw err;
640
+ }
641
+ }
642
+
643
+ /**
644
+ * Switch active network
645
+ */
646
+ async switchNetwork(networkId) {
647
+ const network = await this.registry.setActiveNetwork(networkId);
648
+ console.log(`${c('green', '✓')} Active network: ${network.name} (${networkId})`);
649
+ return network;
650
+ }
651
+
652
+ /**
653
+ * Show network status
654
+ */
655
+ async showStatus() {
656
+ const active = this.registry.getActiveNetwork();
657
+ const joined = this.registry.getJoinedNetworks();
658
+
659
+ console.log(`\n${c('bold', 'NETWORK STATUS:')}\n`);
660
+
661
+ if (!active) {
662
+ console.log(` ${c('yellow', '⚠')} No active network`);
663
+ console.log(` ${c('dim', 'Join a network to start contributing')}\n`);
664
+ return;
665
+ }
666
+
667
+ const stats = await this.registry.getNetworkStats(active.id);
668
+
669
+ console.log(`${c('bold', 'Active Network:')}`);
670
+ console.log(` ${c('cyan', 'Name:')} ${active.name}`);
671
+ console.log(` ${c('cyan', 'ID:')} ${active.id}`);
672
+ console.log(` ${c('cyan', 'Type:')} ${active.type}`);
673
+ console.log(` ${c('cyan', 'QDAG Nodes:')} ${stats.nodes}`);
674
+ console.log(` ${c('cyan', 'Contributions:')} ${stats.contributions}`);
675
+ console.log(` ${c('cyan', 'Contributors:')} ${stats.contributors}`);
676
+ console.log(` ${c('cyan', 'Total Credits:')} ${stats.credits}`);
677
+ console.log(` ${c('cyan', 'Connected Peers:')} ${stats.peers}`);
678
+
679
+ if (joined.length > 1) {
680
+ console.log(`\n${c('bold', 'Other Joined Networks:')}`);
681
+ for (const network of joined) {
682
+ if (network.id !== active.id) {
683
+ console.log(` ${c('dim', '○')} ${network.name} (${network.id})`);
684
+ }
685
+ }
686
+ }
687
+
688
+ console.log('');
689
+ }
690
+
691
+ /**
692
+ * Get active network directory for contributions
693
+ */
694
+ getActiveNetworkDir() {
695
+ const active = this.registry.getActiveNetwork();
696
+ if (!active) return null;
697
+ return getNetworkDir(active.id);
698
+ }
699
+ }
700
+
701
+ // CLI interface
702
+ async function main() {
703
+ const args = process.argv.slice(2);
704
+ const command = args[0];
705
+
706
+ const registry = new NetworkRegistry();
707
+ await registry.load();
708
+
709
+ if (command === 'list' || command === 'ls') {
710
+ console.log(`\n${c('bold', 'NETWORKS:')}\n`);
711
+
712
+ const networks = registry.listNetworks();
713
+ const active = registry.activeNetwork;
714
+
715
+ for (const network of networks) {
716
+ const isActive = network.id === active;
717
+ const status = network.joined ?
718
+ (isActive ? c('green', '● Active') : c('cyan', '○ Joined')) :
719
+ c('dim', ' Available');
720
+ const typeIcon = network.type === NetworkType.PUBLIC ? '🌐' :
721
+ network.type === NetworkType.PRIVATE ? '🔒' : '🏢';
722
+
723
+ console.log(` ${status} ${typeIcon} ${c('bold', network.name)}`);
724
+ console.log(` ${c('dim', 'ID:')} ${network.id}`);
725
+ if (network.description) {
726
+ console.log(` ${c('dim', network.description)}`);
727
+ }
728
+ console.log('');
729
+ }
730
+
731
+ } else if (command === 'discover') {
732
+ const manager = new MultiNetworkManager(null);
733
+ await manager.initialize();
734
+ await manager.discoverNetworks();
735
+
736
+ } else if (command === 'create') {
737
+ const name = args[1] || 'My Network';
738
+ const type = args.includes('--private') ? NetworkType.PRIVATE :
739
+ args.includes('--consortium') ? NetworkType.CONSORTIUM :
740
+ NetworkType.PUBLIC;
741
+ const description = args.find((a, i) => args[i - 1] === '--desc') || '';
742
+
743
+ const manager = new MultiNetworkManager(null);
744
+ await manager.initialize();
745
+ await manager.createNetwork({ name, type, description });
746
+
747
+ } else if (command === 'join') {
748
+ const networkId = args[1];
749
+ const inviteCode = args.find((a, i) => args[i - 1] === '--invite');
750
+
751
+ if (!networkId) {
752
+ console.log(`${c('red', '✗')} Usage: networks join <network-id> [--invite <code>]`);
753
+ process.exit(1);
754
+ }
755
+
756
+ const manager = new MultiNetworkManager(null);
757
+ await manager.initialize();
758
+ await manager.joinNetwork(networkId, inviteCode);
759
+
760
+ } else if (command === 'switch' || command === 'use') {
761
+ const networkId = args[1];
762
+
763
+ if (!networkId) {
764
+ console.log(`${c('red', '✗')} Usage: networks switch <network-id>`);
765
+ process.exit(1);
766
+ }
767
+
768
+ const manager = new MultiNetworkManager(null);
769
+ await manager.initialize();
770
+ await manager.switchNetwork(networkId);
771
+
772
+ } else if (command === 'status') {
773
+ const manager = new MultiNetworkManager(null);
774
+ await manager.initialize();
775
+ await manager.showStatus();
776
+
777
+ } else if (command === 'help' || !command) {
778
+ console.log(`
779
+ ${c('bold', 'Edge-Net Multi-Network Manager')}
780
+
781
+ ${c('bold', 'COMMANDS:')}
782
+ ${c('green', 'list')} List all known networks
783
+ ${c('green', 'discover')} Discover available networks
784
+ ${c('green', 'create')} Create a new network
785
+ ${c('green', 'join')} Join an existing network
786
+ ${c('green', 'switch')} Switch active network
787
+ ${c('green', 'status')} Show current network status
788
+ ${c('green', 'help')} Show this help
789
+
790
+ ${c('bold', 'EXAMPLES:')}
791
+ ${c('dim', '# List networks')}
792
+ $ node networks.js list
793
+
794
+ ${c('dim', '# Create a public network')}
795
+ $ node networks.js create "My Research Network" --desc "For ML research"
796
+
797
+ ${c('dim', '# Create a private network')}
798
+ $ node networks.js create "Team Network" --private
799
+
800
+ ${c('dim', '# Join a network')}
801
+ $ node networks.js join net-abc123def456
802
+
803
+ ${c('dim', '# Join a private network with invite')}
804
+ $ node networks.js join net-xyz789 --invite <invite-code>
805
+
806
+ ${c('dim', '# Switch active network')}
807
+ $ node networks.js switch net-abc123def456
808
+
809
+ ${c('bold', 'NETWORK TYPES:')}
810
+ ${c('cyan', '🌐 Public')} Anyone can join and discover
811
+ ${c('cyan', '🔒 Private')} Requires invite code to join
812
+ ${c('cyan', '🏢 Consortium')} Requires approval from members
813
+ `);
814
+ }
815
+ }
816
+
817
+ main().catch(console.error);