nex-framework-cli 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,964 @@
1
+ /**
2
+ * NEX Marketplace - Complete Agent Marketplace System
3
+ * Integrates with Supabase, local registry, and NEX Store
4
+ */
5
+
6
+ import fs from 'fs-extra'
7
+ import path from 'path'
8
+ import yaml from 'yaml'
9
+ import chalk from 'chalk'
10
+ import ora from 'ora'
11
+ import semver from 'semver'
12
+ import { createClient } from '@supabase/supabase-js'
13
+ import { fileURLToPath } from 'url'
14
+
15
+ const __filename = fileURLToPath(import.meta.url)
16
+ const __dirname = path.dirname(__filename)
17
+
18
+ export default class NEXMarketplace {
19
+ constructor(options = {}) {
20
+ this.projectRoot = options.projectRoot || process.cwd()
21
+ this.registryPath = options.registryPath || path.join(this.projectRoot, 'registry')
22
+ this.installPath = options.installPath || path.join(this.projectRoot, '.nex-core', 'agents')
23
+
24
+ // Supabase client
25
+ this.supabase = null
26
+ this.initializeSupabase()
27
+
28
+ // Config
29
+ this.config = null
30
+ this.loadConfig()
31
+ }
32
+
33
+ /**
34
+ * Initialize Supabase client
35
+ */
36
+ initializeSupabase() {
37
+ const supabaseUrl = process.env.VITE_SUPABASE_URL
38
+ const supabaseKey = process.env.VITE_SUPABASE_ANON_KEY
39
+
40
+ if (supabaseUrl && supabaseKey) {
41
+ this.supabase = createClient(supabaseUrl, supabaseKey)
42
+ } else {
43
+ console.warn(chalk.yellow('⚠️ Supabase not configured. Marketplace will work in local-only mode.'))
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Load registry configuration
49
+ */
50
+ async loadConfig() {
51
+ const configPath = path.join(this.registryPath, '.meta', 'registry.yaml')
52
+
53
+ if (await fs.pathExists(configPath)) {
54
+ const configFile = await fs.readFile(configPath, 'utf8')
55
+ this.config = yaml.parse(configFile)
56
+ } else {
57
+ this.config = this.getDefaultConfig()
58
+ }
59
+
60
+ return this.config
61
+ }
62
+
63
+ /**
64
+ * Default configuration
65
+ */
66
+ getDefaultConfig() {
67
+ return {
68
+ registry: {
69
+ name: 'NEX Expert Agent Marketplace',
70
+ version: '1.0.0',
71
+ type: 'hybrid'
72
+ },
73
+ defaults: {
74
+ install_location: '.nex-core/agents',
75
+ method: 'symlink',
76
+ backup_before_update: true
77
+ }
78
+ }
79
+ }
80
+
81
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
82
+ // SEARCH & DISCOVERY
83
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
84
+
85
+ /**
86
+ * Search for agents
87
+ */
88
+ async search(query, options = {}) {
89
+ const spinner = ora(`Searching for "${query}"...`).start()
90
+
91
+ try {
92
+ let results = []
93
+
94
+ // Search in Supabase if available
95
+ if (this.supabase) {
96
+ results = await this.searchRemote(query, options)
97
+ }
98
+
99
+ // Also search local registry
100
+ const localResults = await this.searchLocal(query, options)
101
+
102
+ // Merge results (deduplicate by agent_id)
103
+ const merged = this.mergeResults(results, localResults)
104
+
105
+ spinner.succeed(`Found ${merged.length} agents`)
106
+
107
+ // Display results
108
+ this.displaySearchResults(merged)
109
+
110
+ return merged
111
+
112
+ } catch (error) {
113
+ spinner.fail('Search failed')
114
+ console.error(chalk.red(`Error: ${error.message}`))
115
+ throw error
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Search in Supabase
121
+ */
122
+ async searchRemote(query, options = {}) {
123
+ let dbQuery = this.supabase
124
+ .from('nex_marketplace_agents')
125
+ .select('*')
126
+ .eq('is_active', true)
127
+
128
+ // Text search in name, description, tags
129
+ if (query) {
130
+ dbQuery = dbQuery.or(`name.ilike.%${query}%,description.ilike.%${query}%,tags.cs.{${query}}`)
131
+ }
132
+
133
+ // Filters
134
+ if (options.category) {
135
+ dbQuery = dbQuery.eq('category', options.category)
136
+ }
137
+
138
+ if (options.official) {
139
+ dbQuery = dbQuery.eq('is_official', true)
140
+ }
141
+
142
+ // Ordering
143
+ const orderBy = options.sort || 'total_installs'
144
+ dbQuery = dbQuery.order(orderBy, { ascending: false })
145
+
146
+ // Limit
147
+ const limit = options.limit || 50
148
+ dbQuery = dbQuery.limit(limit)
149
+
150
+ const { data, error } = await dbQuery
151
+
152
+ if (error) throw error
153
+
154
+ return data || []
155
+ }
156
+
157
+ /**
158
+ * Search in local registry
159
+ */
160
+ async searchLocal(query, options = {}) {
161
+ const results = []
162
+
163
+ const categories = ['planning', 'execution', 'community']
164
+
165
+ for (const category of categories) {
166
+ const categoryPath = path.join(this.registryPath, category)
167
+
168
+ if (!await fs.pathExists(categoryPath)) continue
169
+
170
+ const agents = await fs.readdir(categoryPath)
171
+
172
+ for (const agentId of agents) {
173
+ const manifestPath = path.join(categoryPath, agentId, 'manifest.yaml')
174
+
175
+ if (!await fs.pathExists(manifestPath)) continue
176
+
177
+ const manifestFile = await fs.readFile(manifestPath, 'utf8')
178
+ const manifest = yaml.parse(manifestFile)
179
+
180
+ // Filter by query
181
+ if (query) {
182
+ const searchable = [
183
+ manifest.name,
184
+ manifest.description,
185
+ manifest.tagline,
186
+ ...(manifest.tags || [])
187
+ ].join(' ').toLowerCase()
188
+
189
+ if (!searchable.includes(query.toLowerCase())) {
190
+ continue
191
+ }
192
+ }
193
+
194
+ // Filter by category
195
+ if (options.category && manifest.category !== options.category) {
196
+ continue
197
+ }
198
+
199
+ results.push(manifest)
200
+ }
201
+ }
202
+
203
+ return results
204
+ }
205
+
206
+ /**
207
+ * Merge and deduplicate results
208
+ */
209
+ mergeResults(remote, local) {
210
+ const map = new Map()
211
+
212
+ // Add remote results first (they have stats)
213
+ remote.forEach(agent => {
214
+ map.set(agent.agent_id, { ...agent, source: 'remote' })
215
+ })
216
+
217
+ // Add local results if not already present
218
+ local.forEach(agent => {
219
+ if (!map.has(agent.id)) {
220
+ map.set(agent.id, {
221
+ agent_id: agent.id,
222
+ ...agent,
223
+ source: 'local'
224
+ })
225
+ }
226
+ })
227
+
228
+ return Array.from(map.values())
229
+ }
230
+
231
+ /**
232
+ * Display search results
233
+ */
234
+ displaySearchResults(results) {
235
+ if (results.length === 0) {
236
+ console.log(chalk.yellow('\n😕 No agents found matching your search.\n'))
237
+ return
238
+ }
239
+
240
+ console.log('\n')
241
+
242
+ results.forEach(agent => {
243
+ const icon = agent.icon || '🤖'
244
+ const name = agent.name
245
+ const version = agent.current_version || agent.version
246
+ const tagline = agent.tagline || agent.description?.substring(0, 60) + '...'
247
+
248
+ console.log(chalk.bold.cyan(`${icon} ${name} v${version}`))
249
+ console.log(chalk.gray(` ${tagline}`))
250
+
251
+ if (agent.total_installs || agent.stats?.installs) {
252
+ const installs = agent.total_installs || agent.stats?.installs || 0
253
+ const rating = agent.average_rating || agent.stats?.rating || 0
254
+ console.log(chalk.gray(` ${installs} installs • ⭐ ${rating}/5`))
255
+ }
256
+
257
+ if (agent.tags && agent.tags.length > 0) {
258
+ const tags = Array.isArray(agent.tags) ? agent.tags : Object.values(agent.tags)
259
+ console.log(chalk.gray(` Tags: ${tags.slice(0, 5).join(', ')}`))
260
+ }
261
+
262
+ console.log(chalk.dim(` ${chalk.cyan('nex agent install')} ${agent.agent_id || agent.id}`))
263
+ console.log()
264
+ })
265
+ }
266
+
267
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
268
+ // AGENT INFO
269
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
270
+
271
+ /**
272
+ * Get detailed agent information
273
+ */
274
+ async info(agentId) {
275
+ const spinner = ora(`Loading info for ${agentId}...`).start()
276
+
277
+ try {
278
+ // Try to load from Supabase first
279
+ let agent = null
280
+
281
+ if (this.supabase) {
282
+ const { data } = await this.supabase
283
+ .from('nex_marketplace_agents')
284
+ .select('*')
285
+ .eq('agent_id', agentId)
286
+ .single()
287
+
288
+ agent = data
289
+ }
290
+
291
+ // Fallback to local
292
+ if (!agent) {
293
+ agent = await this.loadLocalManifest(agentId)
294
+ }
295
+
296
+ if (!agent) {
297
+ spinner.fail(`Agent ${agentId} not found`)
298
+ return null
299
+ }
300
+
301
+ spinner.succeed(`Loaded info for ${agentId}`)
302
+
303
+ // Display detailed info
304
+ this.displayAgentInfo(agent)
305
+
306
+ return agent
307
+
308
+ } catch (error) {
309
+ spinner.fail('Failed to load agent info')
310
+ console.error(chalk.red(`Error: ${error.message}`))
311
+ throw error
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Load manifest from local registry
317
+ */
318
+ async loadLocalManifest(agentId) {
319
+ const categories = ['planning', 'execution', 'community']
320
+
321
+ for (const category of categories) {
322
+ const manifestPath = path.join(this.registryPath, category, agentId, 'manifest.yaml')
323
+
324
+ if (await fs.pathExists(manifestPath)) {
325
+ const manifestFile = await fs.readFile(manifestPath, 'utf8')
326
+ const manifest = yaml.parse(manifestFile)
327
+ return { agent_id: agentId, ...manifest }
328
+ }
329
+ }
330
+
331
+ return null
332
+ }
333
+
334
+ /**
335
+ * Display detailed agent information
336
+ */
337
+ displayAgentInfo(agent) {
338
+ const icon = agent.icon || '🤖'
339
+ const name = agent.name
340
+ const version = agent.current_version || agent.version
341
+
342
+ console.log('\n' + chalk.bold.cyan('═'.repeat(60)))
343
+ console.log(chalk.bold.cyan(`${icon} ${name} v${version}`))
344
+ console.log(chalk.bold.cyan('═'.repeat(60)))
345
+
346
+ if (agent.tagline) {
347
+ console.log(chalk.bold('\n💫 Tagline:'))
348
+ console.log(chalk.gray(` ${agent.tagline}`))
349
+ }
350
+
351
+ console.log(chalk.bold('\n📝 Description:'))
352
+ console.log(chalk.gray(` ${agent.description || agent.long_description || 'N/A'}`))
353
+
354
+ if (agent.author_name || agent.author?.name) {
355
+ console.log(chalk.bold('\n👤 Author:'))
356
+ const authorName = agent.author_name || agent.author.name
357
+ const authorEmail = agent.author_email || agent.author?.email
358
+ console.log(chalk.gray(` ${authorName}${authorEmail ? ` <${authorEmail}>` : ''}`))
359
+ }
360
+
361
+ if (agent.tags && agent.tags.length > 0) {
362
+ console.log(chalk.bold('\n🏷️ Tags:'))
363
+ const tags = Array.isArray(agent.tags) ? agent.tags : Object.values(agent.tags)
364
+ console.log(chalk.gray(` ${tags.join(', ')}`))
365
+ }
366
+
367
+ if (agent.capabilities) {
368
+ console.log(chalk.bold('\n⚡ Capabilities:'))
369
+ const capabilities = Array.isArray(agent.capabilities)
370
+ ? agent.capabilities
371
+ : Object.values(agent.capabilities)
372
+ capabilities.slice(0, 10).forEach(cap => {
373
+ console.log(chalk.gray(` • ${cap}`))
374
+ })
375
+ if (capabilities.length > 10) {
376
+ console.log(chalk.gray(` ... and ${capabilities.length - 10} more`))
377
+ }
378
+ }
379
+
380
+ if (agent.total_installs !== undefined) {
381
+ console.log(chalk.bold('\n📊 Stats:'))
382
+ console.log(chalk.gray(` Installs: ${agent.total_installs}`))
383
+ console.log(chalk.gray(` Rating: ${'⭐'.repeat(Math.floor(agent.average_rating || 0))} (${agent.average_rating || 0}/5)`))
384
+ if (agent.total_stars) {
385
+ console.log(chalk.gray(` Stars: ${agent.total_stars}`))
386
+ }
387
+ }
388
+
389
+ if (agent.dependencies?.agents && agent.dependencies.agents.length > 0) {
390
+ console.log(chalk.bold('\n🔗 Dependencies:'))
391
+ agent.dependencies.agents.forEach(dep => {
392
+ console.log(chalk.gray(` • ${dep}`))
393
+ })
394
+ }
395
+
396
+ console.log(chalk.bold('\n🔧 Installation:'))
397
+ console.log(chalk.cyan(` nex agent install ${agent.agent_id || agent.id}`))
398
+
399
+ if (agent.repository_url || agent.links?.repository) {
400
+ console.log(chalk.bold('\n🌐 Links:'))
401
+ const repoUrl = agent.repository_url || agent.links?.repository
402
+ console.log(chalk.gray(` Repository: ${repoUrl}`))
403
+
404
+ if (agent.documentation_url || agent.links?.documentation) {
405
+ const docUrl = agent.documentation_url || agent.links?.documentation
406
+ console.log(chalk.gray(` Documentation: ${docUrl}`))
407
+ }
408
+ }
409
+
410
+ console.log('\n' + chalk.bold.cyan('═'.repeat(60)) + '\n')
411
+ }
412
+
413
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
414
+ // INSTALLATION
415
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
416
+
417
+ /**
418
+ * Install an agent
419
+ */
420
+ async install(agentId, options = {}) {
421
+ const spinner = ora(`Installing ${agentId}...`).start()
422
+
423
+ try {
424
+ // 1. Check if agent is registered in NEX Hub (if Supabase is configured)
425
+ if (this.supabase && !options.skipRegistrationCheck) {
426
+ spinner.text = 'Checking registration...'
427
+
428
+ const { data: { user } } = await this.supabase.auth.getUser()
429
+
430
+ if (user) {
431
+ // User is authenticated, check registration
432
+ const isRegistered = await this.checkAgentRegistration(agentId)
433
+
434
+ if (!isRegistered) {
435
+ spinner.fail('Agent not registered')
436
+ console.log(chalk.yellow(`\n⚠️ Agent "${agentId}" is not registered in your NEX Hub account.\n`))
437
+ console.log(chalk.cyan('💡 Register it first:'))
438
+ console.log(chalk.cyan(` nex agent register ${agentId}\n`))
439
+ console.log(chalk.gray(' Or browse available agents:'))
440
+ console.log(chalk.gray(` nex agent search ${agentId}\n`))
441
+ return false
442
+ }
443
+
444
+ spinner.text = `Installing ${agentId}...`
445
+ } else {
446
+ // User not authenticated, show warning but allow local install
447
+ spinner.warn('Not authenticated - installing locally only')
448
+ console.log(chalk.yellow('\n⚠️ You are not logged in to NEX Hub.'))
449
+ console.log(chalk.gray(' This agent will be installed locally only.\n'))
450
+ }
451
+ }
452
+
453
+ // 2. Resolve version
454
+ const version = options.version || await this.getLatestVersion(agentId)
455
+ spinner.text = `Installing ${agentId}@${version}...`
456
+
457
+ // 3. Load agent manifest
458
+ const manifest = await this.loadLocalManifest(agentId)
459
+
460
+ if (!manifest) {
461
+ spinner.fail(`Agent ${agentId} not found in registry`)
462
+ return false
463
+ }
464
+
465
+ // 4. Check dependencies
466
+ spinner.text = 'Checking dependencies...'
467
+ await this.checkDependencies(manifest)
468
+
469
+ // 5. Determine install location
470
+ const installPath = path.join(this.installPath, agentId)
471
+
472
+ // 5. Check if already installed
473
+ if (await fs.pathExists(installPath)) {
474
+ spinner.info(`Agent ${agentId} already installed`)
475
+
476
+ // Ask user if they want to update
477
+ return await this.update(agentId, options)
478
+ }
479
+
480
+ // 6. Install (symlink or copy)
481
+ const method = options.method || this.config.defaults.method || 'symlink'
482
+ const sourcePath = await this.getAgentSourcePath(agentId, version)
483
+
484
+ spinner.text = `Installing ${agentId} (${method})...`
485
+
486
+ if (method === 'symlink') {
487
+ await fs.ensureDir(path.dirname(installPath))
488
+ await fs.ensureSymlink(sourcePath, installPath)
489
+ } else {
490
+ await fs.copy(sourcePath, installPath)
491
+ }
492
+
493
+ // 7. Track installation
494
+ await this.trackInstallation(agentId, version, method)
495
+
496
+ spinner.succeed(chalk.green(`✅ ${manifest.icon} ${manifest.name} v${version} installed!`))
497
+
498
+ // 8. Show quick start
499
+ console.log(chalk.cyan('\n📖 Quick Start:'))
500
+ console.log(chalk.gray(` @${agentId}`))
501
+ console.log(chalk.gray(` nex agent run ${agentId} <command>\n`))
502
+
503
+ return true
504
+
505
+ } catch (error) {
506
+ spinner.fail(chalk.red(`Failed to install ${agentId}`))
507
+ console.error(chalk.red(`Error: ${error.message}`))
508
+ throw error
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Get agent source path
514
+ */
515
+ async getAgentSourcePath(agentId, version) {
516
+ const categories = ['planning', 'execution', 'community']
517
+
518
+ for (const category of categories) {
519
+ // Try versioned path first
520
+ let sourcePath = path.join(this.registryPath, category, agentId, 'versions', version)
521
+
522
+ if (await fs.pathExists(sourcePath)) {
523
+ return sourcePath
524
+ }
525
+
526
+ // Fallback to non-versioned
527
+ sourcePath = path.join(this.registryPath, category, agentId)
528
+
529
+ if (await fs.pathExists(sourcePath)) {
530
+ return sourcePath
531
+ }
532
+ }
533
+
534
+ throw new Error(`Agent source not found: ${agentId}@${version}`)
535
+ }
536
+
537
+ /**
538
+ * Get latest version of agent
539
+ */
540
+ async getLatestVersion(agentId) {
541
+ // Try Supabase first
542
+ if (this.supabase) {
543
+ const { data } = await this.supabase
544
+ .from('nex_marketplace_versions')
545
+ .select('version')
546
+ .eq('agent_id', agentId)
547
+ .eq('is_latest', true)
548
+ .single()
549
+
550
+ if (data) return data.version
551
+ }
552
+
553
+ // Fallback to manifest
554
+ const manifest = await this.loadLocalManifest(agentId)
555
+ return manifest?.version || manifest?.current_version || '1.0.0'
556
+ }
557
+
558
+ /**
559
+ * Check agent dependencies
560
+ */
561
+ async checkDependencies(manifest) {
562
+ if (!manifest.dependencies || !manifest.dependencies.agents) {
563
+ return true
564
+ }
565
+
566
+ const missing = []
567
+
568
+ for (const depId of manifest.dependencies.agents) {
569
+ const depPath = path.join(this.installPath, depId)
570
+
571
+ if (!await fs.pathExists(depPath)) {
572
+ missing.push(depId)
573
+ }
574
+ }
575
+
576
+ if (missing.length > 0) {
577
+ console.log(chalk.yellow(`\n⚠️ Missing dependencies: ${missing.join(', ')}`))
578
+ console.log(chalk.gray('These agents will be installed automatically.\n'))
579
+
580
+ // Auto-install dependencies
581
+ for (const depId of missing) {
582
+ await this.install(depId)
583
+ }
584
+ }
585
+
586
+ return true
587
+ }
588
+
589
+ /**
590
+ * Track installation
591
+ */
592
+ async trackInstallation(agentId, version, method) {
593
+ // Save to local registry
594
+ const installedFile = path.join(this.installPath, 'installed.json')
595
+ await fs.ensureDir(this.installPath)
596
+
597
+ let installed = {}
598
+ if (await fs.pathExists(installedFile)) {
599
+ installed = await fs.readJSON(installedFile)
600
+ }
601
+
602
+ if (!installed.agents) {
603
+ installed.agents = []
604
+ }
605
+
606
+ // Remove old entry if exists
607
+ installed.agents = installed.agents.filter(a => a.id !== agentId)
608
+
609
+ // Add new entry
610
+ installed.agents.push({
611
+ id: agentId,
612
+ version: version,
613
+ method: method,
614
+ installed_at: new Date().toISOString(),
615
+ project_path: this.projectRoot
616
+ })
617
+
618
+ await fs.writeJSON(installedFile, installed, { spaces: 2 })
619
+
620
+ // Track in Supabase if available
621
+ if (this.supabase) {
622
+ await this.supabase
623
+ .from('nex_marketplace_installs')
624
+ .upsert({
625
+ agent_id: agentId,
626
+ version: version,
627
+ install_method: method,
628
+ project_path: this.projectRoot,
629
+ project_name: path.basename(this.projectRoot),
630
+ is_active: true
631
+ })
632
+ }
633
+ }
634
+
635
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
636
+ // LIST INSTALLED
637
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
638
+
639
+ /**
640
+ * List installed agents
641
+ */
642
+ async list(options = {}) {
643
+ const installedFile = path.join(this.installPath, 'installed.json')
644
+
645
+ if (!await fs.pathExists(installedFile)) {
646
+ console.log(chalk.yellow('📭 No agents installed in this project.'))
647
+ console.log(chalk.gray('\nInstall agents with: nex agent install <agent-id>\n'))
648
+ return []
649
+ }
650
+
651
+ const installed = await fs.readJSON(installedFile)
652
+ const agents = installed.agents || []
653
+
654
+ if (agents.length === 0) {
655
+ console.log(chalk.yellow('📭 No agents installed in this project.\n'))
656
+ return []
657
+ }
658
+
659
+ console.log(chalk.bold.cyan(`\n📦 Installed Agents (${agents.length})`))
660
+ console.log(chalk.gray(`Project: ${path.basename(this.projectRoot)}\n`))
661
+
662
+ for (const agent of agents) {
663
+ const manifest = await this.loadLocalManifest(agent.id)
664
+
665
+ if (manifest) {
666
+ const icon = manifest.icon || '🤖'
667
+ console.log(chalk.bold(`${icon} ${manifest.name || agent.id}`))
668
+ } else {
669
+ console.log(chalk.bold(`🤖 ${agent.id}`))
670
+ }
671
+
672
+ console.log(chalk.gray(` Version: ${agent.version}`))
673
+ console.log(chalk.gray(` Method: ${agent.method}`))
674
+ console.log(chalk.gray(` Installed: ${new Date(agent.installed_at).toLocaleDateString()}`))
675
+ console.log()
676
+ }
677
+
678
+ return agents
679
+ }
680
+
681
+ /**
682
+ * Uninstall agent
683
+ */
684
+ async uninstall(agentId) {
685
+ const spinner = ora(`Uninstalling ${agentId}...`).start()
686
+
687
+ try {
688
+ const installPath = path.join(this.installPath, agentId)
689
+
690
+ if (!await fs.pathExists(installPath)) {
691
+ spinner.fail(`Agent ${agentId} is not installed`)
692
+ return false
693
+ }
694
+
695
+ // Remove files
696
+ await fs.remove(installPath)
697
+
698
+ // Update installed.json
699
+ const installedFile = path.join(this.installPath, 'installed.json')
700
+ const installed = await fs.readJSON(installedFile)
701
+ installed.agents = (installed.agents || []).filter(a => a.id !== agentId)
702
+ await fs.writeJSON(installedFile, installed, { spaces: 2 })
703
+
704
+ // Update Supabase
705
+ if (this.supabase) {
706
+ await this.supabase
707
+ .from('nex_marketplace_installs')
708
+ .update({ is_active: false, uninstalled_at: new Date().toISOString() })
709
+ .eq('agent_id', agentId)
710
+ .eq('project_path', this.projectRoot)
711
+ }
712
+
713
+ spinner.succeed(chalk.green(`✅ Agent ${agentId} uninstalled`))
714
+ return true
715
+
716
+ } catch (error) {
717
+ spinner.fail(`Failed to uninstall ${agentId}`)
718
+ console.error(chalk.red(`Error: ${error.message}`))
719
+ throw error
720
+ }
721
+ }
722
+
723
+ /**
724
+ * Update agent
725
+ */
726
+ async update(agentId, options = {}) {
727
+ console.log(chalk.blue(`🔄 Updating ${agentId}...`))
728
+
729
+ // Uninstall old version
730
+ await this.uninstall(agentId)
731
+
732
+ // Install new version
733
+ return await this.install(agentId, options)
734
+ }
735
+
736
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
737
+ // AGENT REGISTRATION (HUB REGISTRY SYSTEM)
738
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
739
+
740
+ /**
741
+ * Register an agent in the NEX Hub
742
+ */
743
+ async register(agentId) {
744
+ const spinner = ora(`Registering ${agentId} in NEX Hub...`).start()
745
+
746
+ try {
747
+ // 1. Check if Supabase is configured
748
+ if (!this.supabase) {
749
+ spinner.fail('Supabase not configured')
750
+ console.log(chalk.yellow('\n⚠️ NEX Hub requires Supabase configuration.'))
751
+ console.log(chalk.cyan('💡 Set VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY environment variables.\n'))
752
+ return false
753
+ }
754
+
755
+ // 2. Check if user is authenticated
756
+ const { data: { user }, error: authError } = await this.supabase.auth.getUser()
757
+
758
+ if (authError || !user) {
759
+ spinner.fail('Not authenticated')
760
+ console.log(chalk.yellow('\n⚠️ You need to be logged in to register agents.'))
761
+ console.log(chalk.cyan('💡 Run: nex auth login\n'))
762
+ return false
763
+ }
764
+
765
+ // 3. Get agent info from database
766
+ const { data: agent, error: agentError } = await this.supabase
767
+ .from('nex_marketplace_agents')
768
+ .select('agent_id, name, agent_type, icon')
769
+ .eq('agent_id', agentId)
770
+ .single()
771
+
772
+ if (agentError || !agent) {
773
+ spinner.fail(`Agent ${agentId} not found`)
774
+ console.log(chalk.yellow(`\n⚠️ Agent "${agentId}" not found in NEX Hub.`))
775
+ console.log(chalk.cyan(`💡 Search for agents: nex agent search ${agentId}\n`))
776
+ return false
777
+ }
778
+
779
+ // 4. Call Edge Function to register
780
+ const EDGE_FUNCTION_URL = process.env.VITE_SUPABASE_URL?.replace('.supabase.co', '.supabase.co/functions/v1')
781
+ const { data: session } = await this.supabase.auth.getSession()
782
+
783
+ const response = await fetch(`${EDGE_FUNCTION_URL}/nex-agent-registry/register`, {
784
+ method: 'POST',
785
+ headers: {
786
+ 'Content-Type': 'application/json',
787
+ 'Authorization': `Bearer ${session.session?.access_token}`,
788
+ 'apikey': process.env.VITE_SUPABASE_ANON_KEY
789
+ },
790
+ body: JSON.stringify({ agent_id: agentId })
791
+ })
792
+
793
+ const result = await response.json()
794
+
795
+ if (!response.ok) {
796
+ spinner.fail('Registration failed')
797
+
798
+ if (result.details?.reason === 'limit_reached') {
799
+ console.log(chalk.red(`\n❌ ${result.error}\n`))
800
+ console.log(chalk.yellow(`📊 Your Plan: ${result.details.current_plan}`))
801
+ console.log(chalk.yellow(` Agent Type: ${result.details.agent_type}`))
802
+ console.log(chalk.yellow(` Current: ${result.details.current_count}/${result.details.max_allowed}\n`))
803
+
804
+ if (result.details.upgrade_required) {
805
+ console.log(chalk.cyan(`💡 Upgrade to ${result.details.upgrade_required} plan to register more agents.`))
806
+ console.log(chalk.cyan(` Visit: https://nexhub.dev/pricing\n`))
807
+ }
808
+ } else if (result.details?.reason === 'already_registered') {
809
+ spinner.info('Already registered')
810
+ console.log(chalk.blue(`\nℹ️ Agent "${agent.name}" is already registered.\n`))
811
+ return true
812
+ } else {
813
+ console.log(chalk.red(`\n❌ ${result.error}\n`))
814
+ }
815
+
816
+ return false
817
+ }
818
+
819
+ // 5. Success!
820
+ spinner.succeed(chalk.green(`✅ Agent registered successfully!`))
821
+ console.log(chalk.cyan(`\n${agent.icon || '🤖'} ${agent.name} (${agentId})`))
822
+ console.log(chalk.gray(` Type: ${agent.agent_type}`))
823
+ console.log(chalk.gray(` Registration ID: ${result.registration?.registration_id}\n`))
824
+ console.log(chalk.green(`✨ You can now install this agent:`))
825
+ console.log(chalk.cyan(` nex agent install ${agentId}\n`))
826
+
827
+ return true
828
+
829
+ } catch (error) {
830
+ spinner.fail('Registration failed')
831
+ console.error(chalk.red(`\n❌ Error: ${error.message}\n`))
832
+ return false
833
+ }
834
+ }
835
+
836
+ /**
837
+ * List registered agents from NEX Hub
838
+ */
839
+ async listRegisteredAgents() {
840
+ const spinner = ora('Loading your registered agents...').start()
841
+
842
+ try {
843
+ // 1. Check if Supabase is configured
844
+ if (!this.supabase) {
845
+ spinner.fail('Supabase not configured')
846
+ console.log(chalk.yellow('\n⚠️ NEX Hub requires Supabase configuration.\n'))
847
+ return []
848
+ }
849
+
850
+ // 2. Check if user is authenticated
851
+ const { data: { user }, error: authError } = await this.supabase.auth.getUser()
852
+
853
+ if (authError || !user) {
854
+ spinner.fail('Not authenticated')
855
+ console.log(chalk.yellow('\n⚠️ You need to be logged in.\n'))
856
+ return []
857
+ }
858
+
859
+ // 3. Call Edge Function to get registered agents
860
+ const EDGE_FUNCTION_URL = process.env.VITE_SUPABASE_URL?.replace('.supabase.co', '.supabase.co/functions/v1')
861
+ const { data: session } = await this.supabase.auth.getSession()
862
+
863
+ const response = await fetch(`${EDGE_FUNCTION_URL}/nex-agent-registry/my-agents`, {
864
+ method: 'GET',
865
+ headers: {
866
+ 'Authorization': `Bearer ${session.session?.access_token}`,
867
+ 'apikey': process.env.VITE_SUPABASE_ANON_KEY
868
+ }
869
+ })
870
+
871
+ const result = await response.json()
872
+
873
+ if (!response.ok) {
874
+ spinner.fail('Failed to load agents')
875
+ console.log(chalk.red(`\n❌ ${result.error}\n`))
876
+ return []
877
+ }
878
+
879
+ const agents = result.agents || []
880
+
881
+ spinner.succeed(`Found ${agents.length} registered agents`)
882
+
883
+ // 4. Display agents
884
+ if (agents.length === 0) {
885
+ console.log(chalk.yellow('\n😕 You have no agents registered yet.\n'))
886
+ console.log(chalk.cyan('💡 Register an agent:'))
887
+ console.log(chalk.cyan(' nex agent search <query>'))
888
+ console.log(chalk.cyan(' nex agent register <agent-id>\n'))
889
+ return []
890
+ }
891
+
892
+ console.log('\n')
893
+ console.log(chalk.bold.cyan('📦 YOUR REGISTERED AGENTS\n'))
894
+
895
+ agents.forEach(reg => {
896
+ const agent = reg.nex_marketplace_agents
897
+ const icon = agent?.icon || '🤖'
898
+ const name = agent?.name || reg.agent_id
899
+ const type = agent?.agent_type || 'unknown'
900
+ const registeredAt = new Date(reg.registered_at).toLocaleDateString()
901
+
902
+ console.log(chalk.bold(`${icon} ${name}`))
903
+ console.log(chalk.gray(` ID: ${reg.agent_id}`))
904
+ console.log(chalk.gray(` Type: ${type}`))
905
+ console.log(chalk.gray(` Registered: ${registeredAt}`))
906
+ console.log(chalk.dim(` ${chalk.cyan('nex agent install')} ${reg.agent_id}`))
907
+ console.log()
908
+ })
909
+
910
+ // 5. Show plan info
911
+ const { data: planData } = await this.supabase.rpc('get_user_plan', { p_user_id: user.id })
912
+
913
+ if (planData && planData.length > 0) {
914
+ const plan = planData[0]
915
+ const nexCount = agents.filter(a => a.nex_marketplace_agents?.agent_type === 'nex_free').length
916
+ const bmadCount = agents.filter(a => a.nex_marketplace_agents?.agent_type === 'bmad_free').length
917
+ const premiumCount = agents.filter(a => a.nex_marketplace_agents?.agent_type === 'premium').length
918
+
919
+ console.log(chalk.bold.cyan('📊 YOUR PLAN\n'))
920
+ console.log(chalk.bold(` ${plan.plan_name}`))
921
+ console.log(chalk.gray(` NEX Free: ${nexCount}/${plan.max_nex_free}`))
922
+ console.log(chalk.gray(` BMAD Free: ${bmadCount}/${plan.max_bmad_free}`))
923
+ console.log(chalk.gray(` Premium: ${premiumCount}/${plan.max_premium}`))
924
+ console.log()
925
+ }
926
+
927
+ return agents
928
+
929
+ } catch (error) {
930
+ spinner.fail('Failed to load agents')
931
+ console.error(chalk.red(`\n❌ Error: ${error.message}\n`))
932
+ return []
933
+ }
934
+ }
935
+
936
+ /**
937
+ * Check if agent is registered (internal method)
938
+ */
939
+ async checkAgentRegistration(agentId) {
940
+ try {
941
+ if (!this.supabase) return false
942
+
943
+ const { data: { user } } = await this.supabase.auth.getUser()
944
+ if (!user) return false
945
+
946
+ const EDGE_FUNCTION_URL = process.env.VITE_SUPABASE_URL?.replace('.supabase.co', '.supabase.co/functions/v1')
947
+ const { data: session } = await this.supabase.auth.getSession()
948
+
949
+ const response = await fetch(`${EDGE_FUNCTION_URL}/nex-agent-registry/check/${agentId}`, {
950
+ method: 'GET',
951
+ headers: {
952
+ 'Authorization': `Bearer ${session.session?.access_token}`,
953
+ 'apikey': process.env.VITE_SUPABASE_ANON_KEY
954
+ }
955
+ })
956
+
957
+ const result = await response.json()
958
+ return result.is_registered || false
959
+
960
+ } catch (error) {
961
+ return false
962
+ }
963
+ }
964
+ }