nex-framework-cli 1.0.10 → 1.0.12

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.
@@ -1,1220 +1,1247 @@
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 and API URL
35
- */
36
- initializeSupabase() {
37
- const supabaseUrl = process.env.VITE_SUPABASE_URL
38
- const supabaseKey = process.env.VITE_SUPABASE_ANON_KEY
39
-
40
- // URL padrão da Edge Function (funciona sem configuração)
41
- const DEFAULT_API_URL = 'https://jsazeeeqxxebydghsiad.supabase.co/functions/v1/nex-marketplace-api'
42
-
43
- // API URL da Edge Function (preferida - sem expor anon key)
44
- // Prioridade: 1) NEX_MARKETPLACE_API_URL, 2) Construída a partir de VITE_SUPABASE_URL, 3) Default
45
- this.apiUrl = process.env.NEX_MARKETPLACE_API_URL ||
46
- (supabaseUrl ? `${supabaseUrl}/functions/v1/nex-marketplace-api` : DEFAULT_API_URL)
47
-
48
- // Cliente Supabase (apenas para operações que requerem autenticação)
49
- if (supabaseUrl && supabaseKey) {
50
- this.supabase = createClient(supabaseUrl, supabaseKey)
51
- } else if (this.apiUrl) {
52
- // Se tiver API URL mas não tiver anon key, ainda funciona para leitura
53
- // Não mostra mensagem para não poluir o output
54
- } else {
55
- console.warn(chalk.yellow('⚠️ Supabase not configured. Marketplace will work in local-only mode.'))
56
- }
57
- }
58
-
59
- /**
60
- * Load registry configuration
61
- */
62
- async loadConfig() {
63
- const configPath = path.join(this.registryPath, '.meta', 'registry.yaml')
64
-
65
- if (await fs.pathExists(configPath)) {
66
- const configFile = await fs.readFile(configPath, 'utf8')
67
- this.config = yaml.parse(configFile)
68
- } else {
69
- this.config = this.getDefaultConfig()
70
- }
71
-
72
- return this.config
73
- }
74
-
75
- /**
76
- * Default configuration
77
- */
78
- getDefaultConfig() {
79
- return {
80
- registry: {
81
- name: 'NEX Expert Agent Marketplace',
82
- version: '1.0.0',
83
- type: 'hybrid'
84
- },
85
- defaults: {
86
- install_location: '.nex-core/agents',
87
- method: 'symlink',
88
- backup_before_update: true
89
- }
90
- }
91
- }
92
-
93
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
94
- // SEARCH & DISCOVERY
95
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
96
-
97
- /**
98
- * Search for agents
99
- */
100
- async search(query, options = {}) {
101
- const spinner = ora(`Searching for "${query}"...`).start()
102
-
103
- try {
104
- let results = []
105
- let apiError = null
106
-
107
- // Try remote search first
108
- if (this.apiUrl || this.supabase) {
109
- try {
110
- results = await this.searchRemote(query, options)
111
- } catch (error) {
112
- apiError = error
113
- // Continue with local search only
114
- console.warn(chalk.yellow(`\n⚠️ Could not connect to marketplace: ${error.message}`))
115
- console.log(chalk.gray(' Searching local registry only...\n'))
116
- }
117
- }
118
-
119
- // Always search local registry
120
- const localResults = await this.searchLocal(query, options)
121
-
122
- // Merge results (deduplicate by agent_id)
123
- const merged = this.mergeResults(results, localResults)
124
-
125
- if (merged.length > 0) {
126
- spinner.succeed(`Found ${merged.length} agents`)
127
- } else {
128
- spinner.warn('No agents found')
129
- }
130
-
131
- // Display results
132
- this.displaySearchResults(merged)
133
-
134
- // Show warning if API failed
135
- if (apiError && localResults.length > 0) {
136
- console.log(chalk.yellow('\n💡 Tip: Some results may be missing. Check your connection.\n'))
137
- }
138
-
139
- return merged
140
-
141
- } catch (error) {
142
- spinner.fail('Search failed')
143
- console.error(chalk.red(`\nError: ${error.message}`))
144
- console.log(chalk.gray('\n💡 Try searching locally or check your connection.\n'))
145
- throw error
146
- }
147
- }
148
-
149
- /**
150
- * Search in Supabase (via Edge Function ou cliente direto)
151
- */
152
- async searchRemote(query, options = {}) {
153
- // Preferir Edge Function (mais seguro - sem anon key)
154
- if (this.apiUrl) {
155
- return await this.searchViaAPI(query, options)
156
- }
157
-
158
- // Fallback para cliente direto (se anon key disponível)
159
- if (this.supabase) {
160
- return await this.searchViaClient(query, options)
161
- }
162
-
163
- return []
164
- }
165
-
166
- /**
167
- * Search via Edge Function API (SEGURO - sem anon key)
168
- */
169
- async searchViaAPI(query, options = {}) {
170
- try {
171
- const params = new URLSearchParams()
172
- if (query) params.append('q', query)
173
- if (options.category) params.append('category', options.category)
174
- if (options.official) params.append('official', 'true')
175
- if (options.limit) params.append('limit', options.limit.toString())
176
-
177
- // Timeout de 10 segundos
178
- const controller = new AbortController()
179
- const timeoutId = setTimeout(() => controller.abort(), 10000)
180
-
181
- const response = await fetch(`${this.apiUrl}/search?${params}`, {
182
- signal: controller.signal
183
- })
184
-
185
- clearTimeout(timeoutId)
186
-
187
- if (!response.ok) {
188
- throw new Error(`API error: ${response.statusText}`)
189
- }
190
-
191
- const { agents } = await response.json()
192
- return agents || []
193
- } catch (error) {
194
- if (error.name === 'AbortError') {
195
- throw new Error('Connection timeout. The marketplace API is not responding.')
196
- }
197
- if (error.code === 'UND_ERR_CONNECT_TIMEOUT' || error.message.includes('timeout')) {
198
- throw new Error('Connection timeout. Please check your internet connection or try again later.')
199
- }
200
- throw error
201
- }
202
- }
203
-
204
- /**
205
- * List all available agents (browse marketplace)
206
- */
207
- async listAll(options = {}) {
208
- const spinner = ora('Loading available agents...').start()
209
-
210
- try {
211
- let results = []
212
- let apiError = null
213
-
214
- // Try API/Remote first
215
- if (this.apiUrl) {
216
- try {
217
- results = await this.listAllViaAPI(options)
218
- } catch (error) {
219
- apiError = error
220
- console.warn(chalk.yellow(`\n⚠️ Could not connect to marketplace API: ${error.message}`))
221
- console.log(chalk.gray(' Falling back to local registry...\n'))
222
-
223
- // Try Supabase client as fallback
224
- if (this.supabase) {
225
- try {
226
- results = await this.listAllViaClient(options)
227
- } catch (clientError) {
228
- // If both fail, continue with local only
229
- results = []
230
- }
231
- }
232
- }
233
- } else if (this.supabase) {
234
- try {
235
- results = await this.listAllViaClient(options)
236
- } catch (error) {
237
- apiError = error
238
- results = []
239
- }
240
- }
241
-
242
- // Always include local registry
243
- const localResults = await this.searchLocal('', options)
244
-
245
- // Merge results
246
- const merged = this.mergeResults(results, localResults)
247
-
248
- if (merged.length > 0) {
249
- spinner.succeed(`Found ${merged.length} available agents`)
250
- } else if (apiError && localResults.length === 0) {
251
- spinner.warn('No agents found (API unavailable and no local registry)')
252
- } else {
253
- spinner.warn('No agents found')
254
- }
255
-
256
- // Display results
257
- this.displaySearchResults(merged)
258
-
259
- // Show helpful messages
260
- if (apiError) {
261
- if (localResults.length > 0) {
262
- console.log(chalk.yellow('\n💡 Tip: Some agents may be missing. Check your connection or configure NEX_MARKETPLACE_API_URL\n'))
263
- } else {
264
- console.log(chalk.yellow('\n💡 Troubleshooting:'))
265
- console.log(chalk.gray(' • The marketplace API is not responding'))
266
- console.log(chalk.gray(' • No local registry found in this project'))
267
- console.log(chalk.gray(' • Try: nex agent list (shows only installed agents)'))
268
- console.log(chalk.gray(' • Or configure: export NEX_MARKETPLACE_API_URL=<your-url>'))
269
- console.log(chalk.gray(' • Check your internet connection and try again later\n'))
270
- }
271
- }
272
-
273
- return merged
274
-
275
- } catch (error) {
276
- spinner.fail('Failed to load agents')
277
- console.error(chalk.red(`\nError: ${error.message}`))
278
- console.log(chalk.gray('\n💡 Troubleshooting:'))
279
- console.log(chalk.gray(' 1. Check your internet connection'))
280
- console.log(chalk.gray(' 2. Verify the API URL: ' + (this.apiUrl || 'not configured')))
281
- console.log(chalk.gray(' 3. Try: nex agent list (shows only installed agents)'))
282
- console.log(chalk.gray(' 4. Configure custom API: export NEX_MARKETPLACE_API_URL=<your-url>\n'))
283
- throw error
284
- }
285
- }
286
-
287
- /**
288
- * List all agents via Edge Function API
289
- */
290
- async listAllViaAPI(options = {}) {
291
- try {
292
- // Timeout de 10 segundos
293
- const controller = new AbortController()
294
- const timeoutId = setTimeout(() => controller.abort(), 10000)
295
-
296
- const response = await fetch(`${this.apiUrl}/list`, {
297
- signal: controller.signal
298
- })
299
-
300
- clearTimeout(timeoutId)
301
-
302
- if (!response.ok) {
303
- throw new Error(`API error: ${response.statusText}`)
304
- }
305
-
306
- const { agents } = await response.json()
307
- return agents || []
308
- } catch (error) {
309
- if (error.name === 'AbortError') {
310
- throw new Error('Connection timeout. The marketplace API is not responding.')
311
- }
312
- if (error.code === 'UND_ERR_CONNECT_TIMEOUT' || error.message.includes('timeout')) {
313
- throw new Error('Connection timeout. Please check your internet connection or try again later.')
314
- }
315
- throw error
316
- }
317
- }
318
-
319
- /**
320
- * List all agents via Supabase client (fallback)
321
- */
322
- async listAllViaClient(options = {}) {
323
- let query = this.supabase
324
- .from('nex_marketplace_agents')
325
- .select('*')
326
- .eq('is_active', true)
327
- .order('total_installs', { ascending: false })
328
-
329
- if (options.category) {
330
- query = query.eq('category', options.category)
331
- }
332
-
333
- if (options.official) {
334
- query = query.eq('is_official', true)
335
- }
336
-
337
- const limit = options.limit || 100
338
- query = query.limit(limit)
339
-
340
- const { data, error } = await query
341
-
342
- if (error) throw error
343
-
344
- return data || []
345
- }
346
-
347
- /**
348
- * Search via Supabase client (fallback - requer anon key)
349
- */
350
- async searchViaClient(query, options = {}) {
351
- let dbQuery = this.supabase
352
- .from('nex_marketplace_agents')
353
- .select('*')
354
- .eq('is_active', true)
355
-
356
- // Text search in name, description, tags
357
- if (query) {
358
- dbQuery = dbQuery.or(`name.ilike.%${query}%,description.ilike.%${query}%,tags.cs.{${query}}`)
359
- }
360
-
361
- // Filters
362
- if (options.category) {
363
- dbQuery = dbQuery.eq('category', options.category)
364
- }
365
-
366
- if (options.official) {
367
- dbQuery = dbQuery.eq('is_official', true)
368
- }
369
-
370
- // Ordering
371
- const orderBy = options.sort || 'total_installs'
372
- dbQuery = dbQuery.order(orderBy, { ascending: false })
373
-
374
- // Limit
375
- const limit = options.limit || 50
376
- dbQuery = dbQuery.limit(limit)
377
-
378
- const { data, error } = await dbQuery
379
-
380
- if (error) throw error
381
-
382
- return data || []
383
- }
384
-
385
- /**
386
- * Search in local registry
387
- */
388
- async searchLocal(query, options = {}) {
389
- const results = []
390
-
391
- // Check if registry path exists
392
- if (!await fs.pathExists(this.registryPath)) {
393
- return results
394
- }
395
-
396
- const categories = ['planning', 'execution', 'community']
397
-
398
- for (const category of categories) {
399
- const categoryPath = path.join(this.registryPath, category)
400
-
401
- if (!await fs.pathExists(categoryPath)) continue
402
-
403
- try {
404
- const agents = await fs.readdir(categoryPath)
405
-
406
- for (const agentId of agents) {
407
- const manifestPath = path.join(categoryPath, agentId, 'manifest.yaml')
408
-
409
- if (!await fs.pathExists(manifestPath)) continue
410
-
411
- try {
412
- const manifestFile = await fs.readFile(manifestPath, 'utf8')
413
- const manifest = yaml.parse(manifestFile)
414
-
415
- // Filter by query
416
- if (query) {
417
- const searchable = [
418
- manifest.name,
419
- manifest.description,
420
- manifest.tagline,
421
- ...(manifest.tags || [])
422
- ].join(' ').toLowerCase()
423
-
424
- if (!searchable.includes(query.toLowerCase())) {
425
- continue
426
- }
427
- }
428
-
429
- // Filter by category
430
- if (options.category && manifest.category !== options.category) {
431
- continue
432
- }
433
-
434
- results.push({
435
- agent_id: agentId,
436
- ...manifest
437
- })
438
- } catch (error) {
439
- // Skip invalid manifests
440
- continue
441
- }
442
- }
443
- } catch (error) {
444
- // Skip categories that can't be read
445
- continue
446
- }
447
- }
448
-
449
- return results
450
- }
451
-
452
- /**
453
- * Merge and deduplicate results
454
- */
455
- mergeResults(remote, local) {
456
- const map = new Map()
457
-
458
- // Add remote results first (they have stats)
459
- remote.forEach(agent => {
460
- map.set(agent.agent_id, { ...agent, source: 'remote' })
461
- })
462
-
463
- // Add local results if not already present
464
- local.forEach(agent => {
465
- if (!map.has(agent.id)) {
466
- map.set(agent.id, {
467
- agent_id: agent.id,
468
- ...agent,
469
- source: 'local'
470
- })
471
- }
472
- })
473
-
474
- return Array.from(map.values())
475
- }
476
-
477
- /**
478
- * Display search results
479
- */
480
- displaySearchResults(results) {
481
- if (results.length === 0) {
482
- console.log(chalk.yellow('\n😕 No agents found matching your search.\n'))
483
- return
484
- }
485
-
486
- console.log('\n')
487
-
488
- results.forEach(agent => {
489
- const icon = agent.icon || '🤖'
490
- const name = agent.name
491
- const version = agent.current_version || agent.version
492
- const tagline = agent.tagline || agent.description?.substring(0, 60) + '...'
493
-
494
- console.log(chalk.bold.cyan(`${icon} ${name} v${version}`))
495
- console.log(chalk.gray(` ${tagline}`))
496
-
497
- if (agent.total_installs || agent.stats?.installs) {
498
- const installs = agent.total_installs || agent.stats?.installs || 0
499
- const rating = agent.average_rating || agent.stats?.rating || 0
500
- console.log(chalk.gray(` ${installs} installs • ⭐ ${rating}/5`))
501
- }
502
-
503
- if (agent.tags && agent.tags.length > 0) {
504
- const tags = Array.isArray(agent.tags) ? agent.tags : Object.values(agent.tags)
505
- console.log(chalk.gray(` Tags: ${tags.slice(0, 5).join(', ')}`))
506
- }
507
-
508
- console.log(chalk.dim(` ${chalk.cyan('nex agent install')} ${agent.agent_id || agent.id}`))
509
- console.log()
510
- })
511
- }
512
-
513
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
514
- // AGENT INFO
515
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
516
-
517
- /**
518
- * Get detailed agent information
519
- */
520
- async info(agentId) {
521
- const spinner = ora(`Loading info for ${agentId}...`).start()
522
-
523
- try {
524
- // Try to load from API/Remote first (prefer API)
525
- let agent = null
526
-
527
- if (this.apiUrl) {
528
- try {
529
- const response = await fetch(`${this.apiUrl}/info/${agentId}`)
530
- if (response.ok) {
531
- const { agent: apiAgent } = await response.json()
532
- agent = apiAgent
533
- }
534
- } catch (error) {
535
- // Silently fallback
536
- }
537
- } else if (this.supabase) {
538
- const { data } = await this.supabase
539
- .from('nex_marketplace_agents')
540
- .select('*')
541
- .eq('agent_id', agentId)
542
- .single()
543
-
544
- agent = data
545
- }
546
-
547
- // Fallback to local
548
- if (!agent) {
549
- agent = await this.loadLocalManifest(agentId)
550
- }
551
-
552
- if (!agent) {
553
- spinner.fail(`Agent ${agentId} not found`)
554
- return null
555
- }
556
-
557
- spinner.succeed(`Loaded info for ${agentId}`)
558
-
559
- // Display detailed info
560
- this.displayAgentInfo(agent)
561
-
562
- return agent
563
-
564
- } catch (error) {
565
- spinner.fail('Failed to load agent info')
566
- console.error(chalk.red(`Error: ${error.message}`))
567
- throw error
568
- }
569
- }
570
-
571
- /**
572
- * Load manifest from local registry
573
- */
574
- async loadLocalManifest(agentId) {
575
- const categories = ['planning', 'execution', 'community']
576
-
577
- for (const category of categories) {
578
- const manifestPath = path.join(this.registryPath, category, agentId, 'manifest.yaml')
579
-
580
- if (await fs.pathExists(manifestPath)) {
581
- const manifestFile = await fs.readFile(manifestPath, 'utf8')
582
- const manifest = yaml.parse(manifestFile)
583
- return { agent_id: agentId, ...manifest }
584
- }
585
- }
586
-
587
- return null
588
- }
589
-
590
- /**
591
- * Display detailed agent information
592
- */
593
- displayAgentInfo(agent) {
594
- const icon = agent.icon || '🤖'
595
- const name = agent.name
596
- const version = agent.current_version || agent.version
597
-
598
- console.log('\n' + chalk.bold.cyan('═'.repeat(60)))
599
- console.log(chalk.bold.cyan(`${icon} ${name} v${version}`))
600
- console.log(chalk.bold.cyan('═'.repeat(60)))
601
-
602
- if (agent.tagline) {
603
- console.log(chalk.bold('\n💫 Tagline:'))
604
- console.log(chalk.gray(` ${agent.tagline}`))
605
- }
606
-
607
- console.log(chalk.bold('\n📝 Description:'))
608
- console.log(chalk.gray(` ${agent.description || agent.long_description || 'N/A'}`))
609
-
610
- if (agent.author_name || agent.author?.name) {
611
- console.log(chalk.bold('\n👤 Author:'))
612
- const authorName = agent.author_name || agent.author.name
613
- const authorEmail = agent.author_email || agent.author?.email
614
- console.log(chalk.gray(` ${authorName}${authorEmail ? ` <${authorEmail}>` : ''}`))
615
- }
616
-
617
- if (agent.tags && agent.tags.length > 0) {
618
- console.log(chalk.bold('\n🏷️ Tags:'))
619
- const tags = Array.isArray(agent.tags) ? agent.tags : Object.values(agent.tags)
620
- console.log(chalk.gray(` ${tags.join(', ')}`))
621
- }
622
-
623
- if (agent.capabilities) {
624
- console.log(chalk.bold('\n⚡ Capabilities:'))
625
- const capabilities = Array.isArray(agent.capabilities)
626
- ? agent.capabilities
627
- : Object.values(agent.capabilities)
628
- capabilities.slice(0, 10).forEach(cap => {
629
- console.log(chalk.gray(` • ${cap}`))
630
- })
631
- if (capabilities.length > 10) {
632
- console.log(chalk.gray(` ... and ${capabilities.length - 10} more`))
633
- }
634
- }
635
-
636
- if (agent.total_installs !== undefined) {
637
- console.log(chalk.bold('\n📊 Stats:'))
638
- console.log(chalk.gray(` Installs: ${agent.total_installs}`))
639
- console.log(chalk.gray(` Rating: ${'⭐'.repeat(Math.floor(agent.average_rating || 0))} (${agent.average_rating || 0}/5)`))
640
- if (agent.total_stars) {
641
- console.log(chalk.gray(` Stars: ${agent.total_stars}`))
642
- }
643
- }
644
-
645
- if (agent.dependencies?.agents && agent.dependencies.agents.length > 0) {
646
- console.log(chalk.bold('\n🔗 Dependencies:'))
647
- agent.dependencies.agents.forEach(dep => {
648
- console.log(chalk.gray(` • ${dep}`))
649
- })
650
- }
651
-
652
- console.log(chalk.bold('\n🔧 Installation:'))
653
- console.log(chalk.cyan(` nex agent install ${agent.agent_id || agent.id}`))
654
-
655
- if (agent.repository_url || agent.links?.repository) {
656
- console.log(chalk.bold('\n🌐 Links:'))
657
- const repoUrl = agent.repository_url || agent.links?.repository
658
- console.log(chalk.gray(` Repository: ${repoUrl}`))
659
-
660
- if (agent.documentation_url || agent.links?.documentation) {
661
- const docUrl = agent.documentation_url || agent.links?.documentation
662
- console.log(chalk.gray(` Documentation: ${docUrl}`))
663
- }
664
- }
665
-
666
- console.log('\n' + chalk.bold.cyan(''.repeat(60)) + '\n')
667
- }
668
-
669
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
670
- // INSTALLATION
671
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
672
-
673
- /**
674
- * Install an agent
675
- */
676
- async install(agentId, options = {}) {
677
- const spinner = ora(`Installing ${agentId}...`).start()
678
-
679
- try {
680
- // 1. Check if agent is registered in NEX Hub (if Supabase is configured)
681
- if (this.supabase && !options.skipRegistrationCheck) {
682
- spinner.text = 'Checking registration...'
683
-
684
- const { data: { user } } = await this.supabase.auth.getUser()
685
-
686
- if (user) {
687
- // User is authenticated, check registration
688
- const isRegistered = await this.checkAgentRegistration(agentId)
689
-
690
- if (!isRegistered) {
691
- spinner.fail('Agent not registered')
692
- console.log(chalk.yellow(`\n⚠️ Agent "${agentId}" is not registered in your NEX Hub account.\n`))
693
- console.log(chalk.cyan('💡 Register it first:'))
694
- console.log(chalk.cyan(` nex agent register ${agentId}\n`))
695
- console.log(chalk.gray(' Or browse available agents:'))
696
- console.log(chalk.gray(` nex agent search ${agentId}\n`))
697
- return false
698
- }
699
-
700
- spinner.text = `Installing ${agentId}...`
701
- } else {
702
- // User not authenticated, show warning but allow local install
703
- spinner.warn('Not authenticated - installing locally only')
704
- console.log(chalk.yellow('\n⚠️ You are not logged in to NEX Hub.'))
705
- console.log(chalk.gray(' This agent will be installed locally only.\n'))
706
- }
707
- }
708
-
709
- // 2. Resolve version
710
- const version = options.version || await this.getLatestVersion(agentId)
711
- spinner.text = `Installing ${agentId}@${version}...`
712
-
713
- // 3. Load agent manifest
714
- const manifest = await this.loadLocalManifest(agentId)
715
-
716
- if (!manifest) {
717
- spinner.fail(`Agent ${agentId} not found in registry`)
718
- return false
719
- }
720
-
721
- // 4. Check dependencies
722
- spinner.text = 'Checking dependencies...'
723
- await this.checkDependencies(manifest)
724
-
725
- // 5. Determine install location
726
- const installPath = path.join(this.installPath, agentId)
727
-
728
- // 5. Check if already installed
729
- if (await fs.pathExists(installPath)) {
730
- spinner.info(`Agent ${agentId} already installed`)
731
-
732
- // Ask user if they want to update
733
- return await this.update(agentId, options)
734
- }
735
-
736
- // 6. Install (symlink or copy)
737
- const method = options.method || this.config.defaults.method || 'symlink'
738
- const sourcePath = await this.getAgentSourcePath(agentId, version)
739
-
740
- spinner.text = `Installing ${agentId} (${method})...`
741
-
742
- if (method === 'symlink') {
743
- await fs.ensureDir(path.dirname(installPath))
744
- await fs.ensureSymlink(sourcePath, installPath)
745
- } else {
746
- await fs.copy(sourcePath, installPath)
747
- }
748
-
749
- // 7. Track installation
750
- await this.trackInstallation(agentId, version, method)
751
-
752
- spinner.succeed(chalk.green(`✅ ${manifest.icon} ${manifest.name} v${version} installed!`))
753
-
754
- // 8. Show quick start
755
- console.log(chalk.cyan('\n📖 Quick Start:'))
756
- console.log(chalk.gray(` @${agentId}`))
757
- console.log(chalk.gray(` nex agent run ${agentId} <command>\n`))
758
-
759
- return true
760
-
761
- } catch (error) {
762
- spinner.fail(chalk.red(`Failed to install ${agentId}`))
763
- console.error(chalk.red(`Error: ${error.message}`))
764
- throw error
765
- }
766
- }
767
-
768
- /**
769
- * Get agent source path
770
- */
771
- async getAgentSourcePath(agentId, version) {
772
- const categories = ['planning', 'execution', 'community']
773
-
774
- for (const category of categories) {
775
- // Try versioned path first
776
- let sourcePath = path.join(this.registryPath, category, agentId, 'versions', version)
777
-
778
- if (await fs.pathExists(sourcePath)) {
779
- return sourcePath
780
- }
781
-
782
- // Fallback to non-versioned
783
- sourcePath = path.join(this.registryPath, category, agentId)
784
-
785
- if (await fs.pathExists(sourcePath)) {
786
- return sourcePath
787
- }
788
- }
789
-
790
- throw new Error(`Agent source not found: ${agentId}@${version}`)
791
- }
792
-
793
- /**
794
- * Get latest version of agent
795
- */
796
- async getLatestVersion(agentId) {
797
- // Try Supabase first
798
- if (this.supabase) {
799
- const { data } = await this.supabase
800
- .from('nex_marketplace_versions')
801
- .select('version')
802
- .eq('agent_id', agentId)
803
- .eq('is_latest', true)
804
- .single()
805
-
806
- if (data) return data.version
807
- }
808
-
809
- // Fallback to manifest
810
- const manifest = await this.loadLocalManifest(agentId)
811
- return manifest?.version || manifest?.current_version || '1.0.0'
812
- }
813
-
814
- /**
815
- * Check agent dependencies
816
- */
817
- async checkDependencies(manifest) {
818
- if (!manifest.dependencies || !manifest.dependencies.agents) {
819
- return true
820
- }
821
-
822
- const missing = []
823
-
824
- for (const depId of manifest.dependencies.agents) {
825
- const depPath = path.join(this.installPath, depId)
826
-
827
- if (!await fs.pathExists(depPath)) {
828
- missing.push(depId)
829
- }
830
- }
831
-
832
- if (missing.length > 0) {
833
- console.log(chalk.yellow(`\n⚠️ Missing dependencies: ${missing.join(', ')}`))
834
- console.log(chalk.gray('These agents will be installed automatically.\n'))
835
-
836
- // Auto-install dependencies
837
- for (const depId of missing) {
838
- await this.install(depId)
839
- }
840
- }
841
-
842
- return true
843
- }
844
-
845
- /**
846
- * Track installation
847
- */
848
- async trackInstallation(agentId, version, method) {
849
- // Save to local registry
850
- const installedFile = path.join(this.installPath, 'installed.json')
851
- await fs.ensureDir(this.installPath)
852
-
853
- let installed = {}
854
- if (await fs.pathExists(installedFile)) {
855
- installed = await fs.readJSON(installedFile)
856
- }
857
-
858
- if (!installed.agents) {
859
- installed.agents = []
860
- }
861
-
862
- // Remove old entry if exists
863
- installed.agents = installed.agents.filter(a => a.id !== agentId)
864
-
865
- // Add new entry
866
- installed.agents.push({
867
- id: agentId,
868
- version: version,
869
- method: method,
870
- installed_at: new Date().toISOString(),
871
- project_path: this.projectRoot
872
- })
873
-
874
- await fs.writeJSON(installedFile, installed, { spaces: 2 })
875
-
876
- // Track in Supabase if available
877
- if (this.supabase) {
878
- await this.supabase
879
- .from('nex_marketplace_installs')
880
- .upsert({
881
- agent_id: agentId,
882
- version: version,
883
- install_method: method,
884
- project_path: this.projectRoot,
885
- project_name: path.basename(this.projectRoot),
886
- is_active: true
887
- })
888
- }
889
- }
890
-
891
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
892
- // LIST INSTALLED
893
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
894
-
895
- /**
896
- * List installed agents
897
- */
898
- async list(options = {}) {
899
- const installedFile = path.join(this.installPath, 'installed.json')
900
-
901
- if (!await fs.pathExists(installedFile)) {
902
- console.log(chalk.yellow('📭 No agents installed in this project.'))
903
- console.log(chalk.gray('\nInstall agents with: nex agent install <agent-id>\n'))
904
- return []
905
- }
906
-
907
- const installed = await fs.readJSON(installedFile)
908
- const agents = installed.agents || []
909
-
910
- if (agents.length === 0) {
911
- console.log(chalk.yellow('📭 No agents installed in this project.\n'))
912
- return []
913
- }
914
-
915
- console.log(chalk.bold.cyan(`\n📦 Installed Agents (${agents.length})`))
916
- console.log(chalk.gray(`Project: ${path.basename(this.projectRoot)}\n`))
917
-
918
- for (const agent of agents) {
919
- const manifest = await this.loadLocalManifest(agent.id)
920
-
921
- if (manifest) {
922
- const icon = manifest.icon || '🤖'
923
- console.log(chalk.bold(`${icon} ${manifest.name || agent.id}`))
924
- } else {
925
- console.log(chalk.bold(`🤖 ${agent.id}`))
926
- }
927
-
928
- console.log(chalk.gray(` Version: ${agent.version}`))
929
- console.log(chalk.gray(` Method: ${agent.method}`))
930
- console.log(chalk.gray(` Installed: ${new Date(agent.installed_at).toLocaleDateString()}`))
931
- console.log()
932
- }
933
-
934
- return agents
935
- }
936
-
937
- /**
938
- * Uninstall agent
939
- */
940
- async uninstall(agentId) {
941
- const spinner = ora(`Uninstalling ${agentId}...`).start()
942
-
943
- try {
944
- const installPath = path.join(this.installPath, agentId)
945
-
946
- if (!await fs.pathExists(installPath)) {
947
- spinner.fail(`Agent ${agentId} is not installed`)
948
- return false
949
- }
950
-
951
- // Remove files
952
- await fs.remove(installPath)
953
-
954
- // Update installed.json
955
- const installedFile = path.join(this.installPath, 'installed.json')
956
- const installed = await fs.readJSON(installedFile)
957
- installed.agents = (installed.agents || []).filter(a => a.id !== agentId)
958
- await fs.writeJSON(installedFile, installed, { spaces: 2 })
959
-
960
- // Update Supabase
961
- if (this.supabase) {
962
- await this.supabase
963
- .from('nex_marketplace_installs')
964
- .update({ is_active: false, uninstalled_at: new Date().toISOString() })
965
- .eq('agent_id', agentId)
966
- .eq('project_path', this.projectRoot)
967
- }
968
-
969
- spinner.succeed(chalk.green(`✅ Agent ${agentId} uninstalled`))
970
- return true
971
-
972
- } catch (error) {
973
- spinner.fail(`Failed to uninstall ${agentId}`)
974
- console.error(chalk.red(`Error: ${error.message}`))
975
- throw error
976
- }
977
- }
978
-
979
- /**
980
- * Update agent
981
- */
982
- async update(agentId, options = {}) {
983
- console.log(chalk.blue(`🔄 Updating ${agentId}...`))
984
-
985
- // Uninstall old version
986
- await this.uninstall(agentId)
987
-
988
- // Install new version
989
- return await this.install(agentId, options)
990
- }
991
-
992
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
993
- // AGENT REGISTRATION (HUB REGISTRY SYSTEM)
994
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
995
-
996
- /**
997
- * Register an agent in the NEX Hub
998
- */
999
- async register(agentId) {
1000
- const spinner = ora(`Registering ${agentId} in NEX Hub...`).start()
1001
-
1002
- try {
1003
- // 1. Check if Supabase is configured
1004
- if (!this.supabase) {
1005
- spinner.fail('Supabase not configured')
1006
- console.log(chalk.yellow('\n⚠️ NEX Hub requires Supabase configuration.'))
1007
- console.log(chalk.cyan('💡 Set VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY environment variables.\n'))
1008
- return false
1009
- }
1010
-
1011
- // 2. Check if user is authenticated
1012
- const { data: { user }, error: authError } = await this.supabase.auth.getUser()
1013
-
1014
- if (authError || !user) {
1015
- spinner.fail('Not authenticated')
1016
- console.log(chalk.yellow('\n⚠️ You need to be logged in to register agents.'))
1017
- console.log(chalk.cyan('💡 Run: nex auth login\n'))
1018
- return false
1019
- }
1020
-
1021
- // 3. Get agent info from database
1022
- const { data: agent, error: agentError } = await this.supabase
1023
- .from('nex_marketplace_agents')
1024
- .select('agent_id, name, agent_type, icon')
1025
- .eq('agent_id', agentId)
1026
- .single()
1027
-
1028
- if (agentError || !agent) {
1029
- spinner.fail(`Agent ${agentId} not found`)
1030
- console.log(chalk.yellow(`\n⚠️ Agent "${agentId}" not found in NEX Hub.`))
1031
- console.log(chalk.cyan(`💡 Search for agents: nex agent search ${agentId}\n`))
1032
- return false
1033
- }
1034
-
1035
- // 4. Call Edge Function to register
1036
- const EDGE_FUNCTION_URL = process.env.VITE_SUPABASE_URL?.replace('.supabase.co', '.supabase.co/functions/v1')
1037
- const { data: session } = await this.supabase.auth.getSession()
1038
-
1039
- const response = await fetch(`${EDGE_FUNCTION_URL}/nex-agent-registry/register`, {
1040
- method: 'POST',
1041
- headers: {
1042
- 'Content-Type': 'application/json',
1043
- 'Authorization': `Bearer ${session.session?.access_token}`,
1044
- 'apikey': process.env.VITE_SUPABASE_ANON_KEY
1045
- },
1046
- body: JSON.stringify({ agent_id: agentId })
1047
- })
1048
-
1049
- const result = await response.json()
1050
-
1051
- if (!response.ok) {
1052
- spinner.fail('Registration failed')
1053
-
1054
- if (result.details?.reason === 'limit_reached') {
1055
- console.log(chalk.red(`\n❌ ${result.error}\n`))
1056
- console.log(chalk.yellow(`📊 Your Plan: ${result.details.current_plan}`))
1057
- console.log(chalk.yellow(` Agent Type: ${result.details.agent_type}`))
1058
- console.log(chalk.yellow(` Current: ${result.details.current_count}/${result.details.max_allowed}\n`))
1059
-
1060
- if (result.details.upgrade_required) {
1061
- console.log(chalk.cyan(`💡 Upgrade to ${result.details.upgrade_required} plan to register more agents.`))
1062
- console.log(chalk.cyan(` Visit: https://nexhub.dev/pricing\n`))
1063
- }
1064
- } else if (result.details?.reason === 'already_registered') {
1065
- spinner.info('Already registered')
1066
- console.log(chalk.blue(`\nℹ️ Agent "${agent.name}" is already registered.\n`))
1067
- return true
1068
- } else {
1069
- console.log(chalk.red(`\n❌ ${result.error}\n`))
1070
- }
1071
-
1072
- return false
1073
- }
1074
-
1075
- // 5. Success!
1076
- spinner.succeed(chalk.green(`✅ Agent registered successfully!`))
1077
- console.log(chalk.cyan(`\n${agent.icon || '🤖'} ${agent.name} (${agentId})`))
1078
- console.log(chalk.gray(` Type: ${agent.agent_type}`))
1079
- console.log(chalk.gray(` Registration ID: ${result.registration?.registration_id}\n`))
1080
- console.log(chalk.green(`✨ You can now install this agent:`))
1081
- console.log(chalk.cyan(` nex agent install ${agentId}\n`))
1082
-
1083
- return true
1084
-
1085
- } catch (error) {
1086
- spinner.fail('Registration failed')
1087
- console.error(chalk.red(`\n❌ Error: ${error.message}\n`))
1088
- return false
1089
- }
1090
- }
1091
-
1092
- /**
1093
- * List registered agents from NEX Hub
1094
- */
1095
- async listRegisteredAgents() {
1096
- const spinner = ora('Loading your registered agents...').start()
1097
-
1098
- try {
1099
- // 1. Check if Supabase is configured
1100
- if (!this.supabase) {
1101
- spinner.fail('Supabase not configured')
1102
- console.log(chalk.yellow('\n⚠️ NEX Hub requires Supabase configuration.\n'))
1103
- return []
1104
- }
1105
-
1106
- // 2. Check if user is authenticated
1107
- const { data: { user }, error: authError } = await this.supabase.auth.getUser()
1108
-
1109
- if (authError || !user) {
1110
- spinner.fail('Not authenticated')
1111
- console.log(chalk.yellow('\n⚠️ You need to be logged in.\n'))
1112
- return []
1113
- }
1114
-
1115
- // 3. Call Edge Function to get registered agents
1116
- const EDGE_FUNCTION_URL = process.env.VITE_SUPABASE_URL?.replace('.supabase.co', '.supabase.co/functions/v1')
1117
- const { data: session } = await this.supabase.auth.getSession()
1118
-
1119
- const response = await fetch(`${EDGE_FUNCTION_URL}/nex-agent-registry/my-agents`, {
1120
- method: 'GET',
1121
- headers: {
1122
- 'Authorization': `Bearer ${session.session?.access_token}`,
1123
- 'apikey': process.env.VITE_SUPABASE_ANON_KEY
1124
- }
1125
- })
1126
-
1127
- const result = await response.json()
1128
-
1129
- if (!response.ok) {
1130
- spinner.fail('Failed to load agents')
1131
- console.log(chalk.red(`\n❌ ${result.error}\n`))
1132
- return []
1133
- }
1134
-
1135
- const agents = result.agents || []
1136
-
1137
- spinner.succeed(`Found ${agents.length} registered agents`)
1138
-
1139
- // 4. Display agents
1140
- if (agents.length === 0) {
1141
- console.log(chalk.yellow('\n😕 You have no agents registered yet.\n'))
1142
- console.log(chalk.cyan('💡 Register an agent:'))
1143
- console.log(chalk.cyan(' nex agent search <query>'))
1144
- console.log(chalk.cyan(' nex agent register <agent-id>\n'))
1145
- return []
1146
- }
1147
-
1148
- console.log('\n')
1149
- console.log(chalk.bold.cyan('📦 YOUR REGISTERED AGENTS\n'))
1150
-
1151
- agents.forEach(reg => {
1152
- const agent = reg.nex_marketplace_agents
1153
- const icon = agent?.icon || '🤖'
1154
- const name = agent?.name || reg.agent_id
1155
- const type = agent?.agent_type || 'unknown'
1156
- const registeredAt = new Date(reg.registered_at).toLocaleDateString()
1157
-
1158
- console.log(chalk.bold(`${icon} ${name}`))
1159
- console.log(chalk.gray(` ID: ${reg.agent_id}`))
1160
- console.log(chalk.gray(` Type: ${type}`))
1161
- console.log(chalk.gray(` Registered: ${registeredAt}`))
1162
- console.log(chalk.dim(` ${chalk.cyan('nex agent install')} ${reg.agent_id}`))
1163
- console.log()
1164
- })
1165
-
1166
- // 5. Show plan info
1167
- const { data: planData } = await this.supabase.rpc('get_user_plan', { p_user_id: user.id })
1168
-
1169
- if (planData && planData.length > 0) {
1170
- const plan = planData[0]
1171
- const nexCount = agents.filter(a => a.nex_marketplace_agents?.agent_type === 'nex_free').length
1172
- const bmadCount = agents.filter(a => a.nex_marketplace_agents?.agent_type === 'bmad_free').length
1173
- const premiumCount = agents.filter(a => a.nex_marketplace_agents?.agent_type === 'premium').length
1174
-
1175
- console.log(chalk.bold.cyan('📊 YOUR PLAN\n'))
1176
- console.log(chalk.bold(` ${plan.plan_name}`))
1177
- console.log(chalk.gray(` NEX Free: ${nexCount}/${plan.max_nex_free}`))
1178
- console.log(chalk.gray(` BMAD Free: ${bmadCount}/${plan.max_bmad_free}`))
1179
- console.log(chalk.gray(` Premium: ${premiumCount}/${plan.max_premium}`))
1180
- console.log()
1181
- }
1182
-
1183
- return agents
1184
-
1185
- } catch (error) {
1186
- spinner.fail('Failed to load agents')
1187
- console.error(chalk.red(`\n❌ Error: ${error.message}\n`))
1188
- return []
1189
- }
1190
- }
1191
-
1192
- /**
1193
- * Check if agent is registered (internal method)
1194
- */
1195
- async checkAgentRegistration(agentId) {
1196
- try {
1197
- if (!this.supabase) return false
1198
-
1199
- const { data: { user } } = await this.supabase.auth.getUser()
1200
- if (!user) return false
1201
-
1202
- const EDGE_FUNCTION_URL = process.env.VITE_SUPABASE_URL?.replace('.supabase.co', '.supabase.co/functions/v1')
1203
- const { data: session } = await this.supabase.auth.getSession()
1204
-
1205
- const response = await fetch(`${EDGE_FUNCTION_URL}/nex-agent-registry/check/${agentId}`, {
1206
- method: 'GET',
1207
- headers: {
1208
- 'Authorization': `Bearer ${session.session?.access_token}`,
1209
- 'apikey': process.env.VITE_SUPABASE_ANON_KEY
1210
- }
1211
- })
1212
-
1213
- const result = await response.json()
1214
- return result.is_registered || false
1215
-
1216
- } catch (error) {
1217
- return false
1218
- }
1219
- }
1220
- }
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 and API URL
35
+ */
36
+ initializeSupabase() {
37
+ const supabaseUrl = process.env.VITE_SUPABASE_URL
38
+ const supabaseKey = process.env.VITE_SUPABASE_ANON_KEY
39
+
40
+ // Guardar anon key para usar nos headers da API
41
+ this.anonKey = supabaseKey || null
42
+
43
+ // URL padrão da Edge Function (funciona sem configuração)
44
+ const DEFAULT_API_URL = 'https://auqfubbpxjzuzzqfazdp.supabase.co/functions/v1/nex-marketplace-api'
45
+
46
+ // API URL da Edge Function (preferida - sem expor anon key)
47
+ // Prioridade: 1) NEX_MARKETPLACE_API_URL, 2) Construída a partir de VITE_SUPABASE_URL, 3) Default
48
+ this.apiUrl = process.env.NEX_MARKETPLACE_API_URL ||
49
+ (supabaseUrl ? `${supabaseUrl}/functions/v1/nex-marketplace-api` : DEFAULT_API_URL)
50
+
51
+ // Cliente Supabase (apenas para operações que requerem autenticação)
52
+ if (supabaseUrl && supabaseKey) {
53
+ this.supabase = createClient(supabaseUrl, supabaseKey)
54
+ } else if (this.apiUrl) {
55
+ // Se tiver API URL mas não tiver anon key, ainda funciona para leitura
56
+ // Não mostra mensagem para não poluir o output
57
+ } else {
58
+ console.warn(chalk.yellow('⚠️ Supabase not configured. Marketplace will work in local-only mode.'))
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Get headers for Edge Function requests
64
+ * Inclui apikey se disponível (requerido pelo gateway do Supabase)
65
+ */
66
+ getApiHeaders() {
67
+ const headers = {
68
+ 'Content-Type': 'application/json',
69
+ 'Accept': 'application/json'
70
+ }
71
+
72
+ // Adicionar apikey se disponível (requerido pelo gateway do Supabase)
73
+ // Mesmo com verify_jwt = false, o gateway pode exigir este header
74
+ if (this.anonKey) {
75
+ headers['apikey'] = this.anonKey
76
+ }
77
+
78
+ return headers
79
+ }
80
+
81
+ /**
82
+ * Load registry configuration
83
+ */
84
+ async loadConfig() {
85
+ const configPath = path.join(this.registryPath, '.meta', 'registry.yaml')
86
+
87
+ if (await fs.pathExists(configPath)) {
88
+ const configFile = await fs.readFile(configPath, 'utf8')
89
+ this.config = yaml.parse(configFile)
90
+ } else {
91
+ this.config = this.getDefaultConfig()
92
+ }
93
+
94
+ return this.config
95
+ }
96
+
97
+ /**
98
+ * Default configuration
99
+ */
100
+ getDefaultConfig() {
101
+ return {
102
+ registry: {
103
+ name: 'NEX Expert Agent Marketplace',
104
+ version: '1.0.0',
105
+ type: 'hybrid'
106
+ },
107
+ defaults: {
108
+ install_location: '.nex-core/agents',
109
+ method: 'symlink',
110
+ backup_before_update: true
111
+ }
112
+ }
113
+ }
114
+
115
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
116
+ // SEARCH & DISCOVERY
117
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
118
+
119
+ /**
120
+ * Search for agents
121
+ */
122
+ async search(query, options = {}) {
123
+ const spinner = ora(`Searching for "${query}"...`).start()
124
+
125
+ try {
126
+ let results = []
127
+ let apiError = null
128
+
129
+ // Try remote search first
130
+ if (this.apiUrl || this.supabase) {
131
+ try {
132
+ results = await this.searchRemote(query, options)
133
+ } catch (error) {
134
+ apiError = error
135
+ // Continue with local search only
136
+ console.warn(chalk.yellow(`\n⚠️ Could not connect to marketplace: ${error.message}`))
137
+ console.log(chalk.gray(' Searching local registry only...\n'))
138
+ }
139
+ }
140
+
141
+ // Always search local registry
142
+ const localResults = await this.searchLocal(query, options)
143
+
144
+ // Merge results (deduplicate by agent_id)
145
+ const merged = this.mergeResults(results, localResults)
146
+
147
+ if (merged.length > 0) {
148
+ spinner.succeed(`Found ${merged.length} agents`)
149
+ } else {
150
+ spinner.warn('No agents found')
151
+ }
152
+
153
+ // Display results
154
+ this.displaySearchResults(merged)
155
+
156
+ // Show warning if API failed
157
+ if (apiError && localResults.length > 0) {
158
+ console.log(chalk.yellow('\n💡 Tip: Some results may be missing. Check your connection.\n'))
159
+ }
160
+
161
+ return merged
162
+
163
+ } catch (error) {
164
+ spinner.fail('Search failed')
165
+ console.error(chalk.red(`\nError: ${error.message}`))
166
+ console.log(chalk.gray('\n💡 Try searching locally or check your connection.\n'))
167
+ throw error
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Search in Supabase (via Edge Function ou cliente direto)
173
+ */
174
+ async searchRemote(query, options = {}) {
175
+ // Preferir Edge Function (mais seguro - sem anon key)
176
+ if (this.apiUrl) {
177
+ return await this.searchViaAPI(query, options)
178
+ }
179
+
180
+ // Fallback para cliente direto (se anon key disponível)
181
+ if (this.supabase) {
182
+ return await this.searchViaClient(query, options)
183
+ }
184
+
185
+ return []
186
+ }
187
+
188
+ /**
189
+ * Search via Edge Function API (SEGURO - sem anon key)
190
+ */
191
+ async searchViaAPI(query, options = {}) {
192
+ try {
193
+ const params = new URLSearchParams()
194
+ if (query) params.append('q', query)
195
+ if (options.category) params.append('category', options.category)
196
+ if (options.official) params.append('official', 'true')
197
+ if (options.limit) params.append('limit', options.limit.toString())
198
+
199
+ // Timeout de 10 segundos
200
+ const controller = new AbortController()
201
+ const timeoutId = setTimeout(() => controller.abort(), 10000)
202
+
203
+ const response = await fetch(`${this.apiUrl}/search?${params}`, {
204
+ signal: controller.signal,
205
+ headers: this.getApiHeaders()
206
+ })
207
+
208
+ clearTimeout(timeoutId)
209
+
210
+ if (!response.ok) {
211
+ throw new Error(`API error: ${response.statusText}`)
212
+ }
213
+
214
+ const { agents } = await response.json()
215
+ return agents || []
216
+ } catch (error) {
217
+ if (error.name === 'AbortError') {
218
+ throw new Error('Connection timeout. The marketplace API is not responding.')
219
+ }
220
+ if (error.code === 'UND_ERR_CONNECT_TIMEOUT' || error.message.includes('timeout')) {
221
+ throw new Error('Connection timeout. Please check your internet connection or try again later.')
222
+ }
223
+ throw error
224
+ }
225
+ }
226
+
227
+ /**
228
+ * List all available agents (browse marketplace)
229
+ */
230
+ async listAll(options = {}) {
231
+ const spinner = ora('Loading available agents...').start()
232
+
233
+ try {
234
+ let results = []
235
+ let apiError = null
236
+
237
+ // Try API/Remote first
238
+ if (this.apiUrl) {
239
+ try {
240
+ results = await this.listAllViaAPI(options)
241
+ } catch (error) {
242
+ apiError = error
243
+ console.warn(chalk.yellow(`\n⚠️ Could not connect to marketplace API: ${error.message}`))
244
+ console.log(chalk.gray(' Falling back to local registry...\n'))
245
+
246
+ // Try Supabase client as fallback
247
+ if (this.supabase) {
248
+ try {
249
+ results = await this.listAllViaClient(options)
250
+ } catch (clientError) {
251
+ // If both fail, continue with local only
252
+ results = []
253
+ }
254
+ }
255
+ }
256
+ } else if (this.supabase) {
257
+ try {
258
+ results = await this.listAllViaClient(options)
259
+ } catch (error) {
260
+ apiError = error
261
+ results = []
262
+ }
263
+ }
264
+
265
+ // Always include local registry
266
+ const localResults = await this.searchLocal('', options)
267
+
268
+ // Merge results
269
+ const merged = this.mergeResults(results, localResults)
270
+
271
+ if (merged.length > 0) {
272
+ spinner.succeed(`Found ${merged.length} available agents`)
273
+ } else if (apiError && localResults.length === 0) {
274
+ spinner.warn('No agents found (API unavailable and no local registry)')
275
+ } else {
276
+ spinner.warn('No agents found')
277
+ }
278
+
279
+ // Display results
280
+ this.displaySearchResults(merged)
281
+
282
+ // Show helpful messages
283
+ if (apiError) {
284
+ if (localResults.length > 0) {
285
+ console.log(chalk.yellow('\n💡 Tip: Some agents may be missing. Check your connection or configure NEX_MARKETPLACE_API_URL\n'))
286
+ } else {
287
+ console.log(chalk.yellow('\n💡 Troubleshooting:'))
288
+ console.log(chalk.gray(' The marketplace API is not responding'))
289
+ console.log(chalk.gray(' • No local registry found in this project'))
290
+ console.log(chalk.gray(' • Try: nex agent list (shows only installed agents)'))
291
+ console.log(chalk.gray(' • Or configure: export NEX_MARKETPLACE_API_URL=<your-url>'))
292
+ console.log(chalk.gray(' • Check your internet connection and try again later\n'))
293
+ }
294
+ }
295
+
296
+ return merged
297
+
298
+ } catch (error) {
299
+ spinner.fail('Failed to load agents')
300
+ console.error(chalk.red(`\nError: ${error.message}`))
301
+ console.log(chalk.gray('\n💡 Troubleshooting:'))
302
+ console.log(chalk.gray(' 1. Check your internet connection'))
303
+ console.log(chalk.gray(' 2. Verify the API URL: ' + (this.apiUrl || 'not configured')))
304
+ console.log(chalk.gray(' 3. Try: nex agent list (shows only installed agents)'))
305
+ console.log(chalk.gray(' 4. Configure custom API: export NEX_MARKETPLACE_API_URL=<your-url>\n'))
306
+ throw error
307
+ }
308
+ }
309
+
310
+ /**
311
+ * List all agents via Edge Function API
312
+ */
313
+ async listAllViaAPI(options = {}) {
314
+ try {
315
+ // Timeout de 10 segundos
316
+ const controller = new AbortController()
317
+ const timeoutId = setTimeout(() => controller.abort(), 10000)
318
+
319
+ const response = await fetch(`${this.apiUrl}/list`, {
320
+ signal: controller.signal,
321
+ headers: this.getApiHeaders()
322
+ })
323
+
324
+ clearTimeout(timeoutId)
325
+
326
+ if (!response.ok) {
327
+ throw new Error(`API error: ${response.statusText}`)
328
+ }
329
+
330
+ const { agents } = await response.json()
331
+ return agents || []
332
+ } catch (error) {
333
+ if (error.name === 'AbortError') {
334
+ throw new Error('Connection timeout. The marketplace API is not responding.')
335
+ }
336
+ if (error.code === 'UND_ERR_CONNECT_TIMEOUT' || error.message.includes('timeout')) {
337
+ throw new Error('Connection timeout. Please check your internet connection or try again later.')
338
+ }
339
+ throw error
340
+ }
341
+ }
342
+
343
+ /**
344
+ * List all agents via Supabase client (fallback)
345
+ */
346
+ async listAllViaClient(options = {}) {
347
+ let query = this.supabase
348
+ .from('nex_marketplace_agents')
349
+ .select('*')
350
+ .eq('is_active', true)
351
+ .order('total_installs', { ascending: false })
352
+
353
+ if (options.category) {
354
+ query = query.eq('category', options.category)
355
+ }
356
+
357
+ if (options.official) {
358
+ query = query.eq('is_official', true)
359
+ }
360
+
361
+ const limit = options.limit || 100
362
+ query = query.limit(limit)
363
+
364
+ const { data, error } = await query
365
+
366
+ if (error) throw error
367
+
368
+ return data || []
369
+ }
370
+
371
+ /**
372
+ * Search via Supabase client (fallback - requer anon key)
373
+ */
374
+ async searchViaClient(query, options = {}) {
375
+ let dbQuery = this.supabase
376
+ .from('nex_marketplace_agents')
377
+ .select('*')
378
+ .eq('is_active', true)
379
+
380
+ // Text search in name, description, tags
381
+ if (query) {
382
+ dbQuery = dbQuery.or(`name.ilike.%${query}%,description.ilike.%${query}%,tags.cs.{${query}}`)
383
+ }
384
+
385
+ // Filters
386
+ if (options.category) {
387
+ dbQuery = dbQuery.eq('category', options.category)
388
+ }
389
+
390
+ if (options.official) {
391
+ dbQuery = dbQuery.eq('is_official', true)
392
+ }
393
+
394
+ // Ordering
395
+ const orderBy = options.sort || 'total_installs'
396
+ dbQuery = dbQuery.order(orderBy, { ascending: false })
397
+
398
+ // Limit
399
+ const limit = options.limit || 50
400
+ dbQuery = dbQuery.limit(limit)
401
+
402
+ const { data, error } = await dbQuery
403
+
404
+ if (error) throw error
405
+
406
+ return data || []
407
+ }
408
+
409
+ /**
410
+ * Search in local registry
411
+ */
412
+ async searchLocal(query, options = {}) {
413
+ const results = []
414
+
415
+ // Check if registry path exists
416
+ if (!await fs.pathExists(this.registryPath)) {
417
+ return results
418
+ }
419
+
420
+ const categories = ['planning', 'execution', 'community']
421
+
422
+ for (const category of categories) {
423
+ const categoryPath = path.join(this.registryPath, category)
424
+
425
+ if (!await fs.pathExists(categoryPath)) continue
426
+
427
+ try {
428
+ const agents = await fs.readdir(categoryPath)
429
+
430
+ for (const agentId of agents) {
431
+ const manifestPath = path.join(categoryPath, agentId, 'manifest.yaml')
432
+
433
+ if (!await fs.pathExists(manifestPath)) continue
434
+
435
+ try {
436
+ const manifestFile = await fs.readFile(manifestPath, 'utf8')
437
+ const manifest = yaml.parse(manifestFile)
438
+
439
+ // Filter by query
440
+ if (query) {
441
+ const searchable = [
442
+ manifest.name,
443
+ manifest.description,
444
+ manifest.tagline,
445
+ ...(manifest.tags || [])
446
+ ].join(' ').toLowerCase()
447
+
448
+ if (!searchable.includes(query.toLowerCase())) {
449
+ continue
450
+ }
451
+ }
452
+
453
+ // Filter by category
454
+ if (options.category && manifest.category !== options.category) {
455
+ continue
456
+ }
457
+
458
+ results.push({
459
+ agent_id: agentId,
460
+ ...manifest
461
+ })
462
+ } catch (error) {
463
+ // Skip invalid manifests
464
+ continue
465
+ }
466
+ }
467
+ } catch (error) {
468
+ // Skip categories that can't be read
469
+ continue
470
+ }
471
+ }
472
+
473
+ return results
474
+ }
475
+
476
+ /**
477
+ * Merge and deduplicate results
478
+ */
479
+ mergeResults(remote, local) {
480
+ const map = new Map()
481
+
482
+ // Add remote results first (they have stats)
483
+ remote.forEach(agent => {
484
+ map.set(agent.agent_id, { ...agent, source: 'remote' })
485
+ })
486
+
487
+ // Add local results if not already present
488
+ local.forEach(agent => {
489
+ if (!map.has(agent.id)) {
490
+ map.set(agent.id, {
491
+ agent_id: agent.id,
492
+ ...agent,
493
+ source: 'local'
494
+ })
495
+ }
496
+ })
497
+
498
+ return Array.from(map.values())
499
+ }
500
+
501
+ /**
502
+ * Display search results
503
+ */
504
+ displaySearchResults(results) {
505
+ if (results.length === 0) {
506
+ console.log(chalk.yellow('\n😕 No agents found matching your search.\n'))
507
+ return
508
+ }
509
+
510
+ console.log('\n')
511
+
512
+ results.forEach(agent => {
513
+ const icon = agent.icon || '🤖'
514
+ const name = agent.name
515
+ const version = agent.current_version || agent.version
516
+ const tagline = agent.tagline || agent.description?.substring(0, 60) + '...'
517
+
518
+ console.log(chalk.bold.cyan(`${icon} ${name} v${version}`))
519
+ console.log(chalk.gray(` ${tagline}`))
520
+
521
+ if (agent.total_installs || agent.stats?.installs) {
522
+ const installs = agent.total_installs || agent.stats?.installs || 0
523
+ const rating = agent.average_rating || agent.stats?.rating || 0
524
+ console.log(chalk.gray(` ${installs} installs ${rating}/5`))
525
+ }
526
+
527
+ if (agent.tags && agent.tags.length > 0) {
528
+ const tags = Array.isArray(agent.tags) ? agent.tags : Object.values(agent.tags)
529
+ console.log(chalk.gray(` Tags: ${tags.slice(0, 5).join(', ')}`))
530
+ }
531
+
532
+ console.log(chalk.dim(` ${chalk.cyan('nex agent install')} ${agent.agent_id || agent.id}`))
533
+ console.log()
534
+ })
535
+ }
536
+
537
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
538
+ // AGENT INFO
539
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
540
+
541
+ /**
542
+ * Get detailed agent information
543
+ */
544
+ async info(agentId) {
545
+ const spinner = ora(`Loading info for ${agentId}...`).start()
546
+
547
+ try {
548
+ // Try to load from API/Remote first (prefer API)
549
+ let agent = null
550
+
551
+ if (this.apiUrl) {
552
+ try {
553
+ const response = await fetch(`${this.apiUrl}/info/${agentId}`, {
554
+ headers: this.getApiHeaders()
555
+ })
556
+
557
+ if (response.ok) {
558
+ const { agent: apiAgent } = await response.json()
559
+ agent = apiAgent
560
+ }
561
+ } catch (error) {
562
+ // Silently fallback
563
+ }
564
+ } else if (this.supabase) {
565
+ const { data } = await this.supabase
566
+ .from('nex_marketplace_agents')
567
+ .select('*')
568
+ .eq('agent_id', agentId)
569
+ .single()
570
+
571
+ agent = data
572
+ }
573
+
574
+ // Fallback to local
575
+ if (!agent) {
576
+ agent = await this.loadLocalManifest(agentId)
577
+ }
578
+
579
+ if (!agent) {
580
+ spinner.fail(`Agent ${agentId} not found`)
581
+ return null
582
+ }
583
+
584
+ spinner.succeed(`Loaded info for ${agentId}`)
585
+
586
+ // Display detailed info
587
+ this.displayAgentInfo(agent)
588
+
589
+ return agent
590
+
591
+ } catch (error) {
592
+ spinner.fail('Failed to load agent info')
593
+ console.error(chalk.red(`Error: ${error.message}`))
594
+ throw error
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Load manifest from local registry
600
+ */
601
+ async loadLocalManifest(agentId) {
602
+ const categories = ['planning', 'execution', 'community']
603
+
604
+ for (const category of categories) {
605
+ const manifestPath = path.join(this.registryPath, category, agentId, 'manifest.yaml')
606
+
607
+ if (await fs.pathExists(manifestPath)) {
608
+ const manifestFile = await fs.readFile(manifestPath, 'utf8')
609
+ const manifest = yaml.parse(manifestFile)
610
+ return { agent_id: agentId, ...manifest }
611
+ }
612
+ }
613
+
614
+ return null
615
+ }
616
+
617
+ /**
618
+ * Display detailed agent information
619
+ */
620
+ displayAgentInfo(agent) {
621
+ const icon = agent.icon || '🤖'
622
+ const name = agent.name
623
+ const version = agent.current_version || agent.version
624
+
625
+ console.log('\n' + chalk.bold.cyan('═'.repeat(60)))
626
+ console.log(chalk.bold.cyan(`${icon} ${name} v${version}`))
627
+ console.log(chalk.bold.cyan('═'.repeat(60)))
628
+
629
+ if (agent.tagline) {
630
+ console.log(chalk.bold('\n💫 Tagline:'))
631
+ console.log(chalk.gray(` ${agent.tagline}`))
632
+ }
633
+
634
+ console.log(chalk.bold('\n📝 Description:'))
635
+ console.log(chalk.gray(` ${agent.description || agent.long_description || 'N/A'}`))
636
+
637
+ if (agent.author_name || agent.author?.name) {
638
+ console.log(chalk.bold('\n👤 Author:'))
639
+ const authorName = agent.author_name || agent.author.name
640
+ const authorEmail = agent.author_email || agent.author?.email
641
+ console.log(chalk.gray(` ${authorName}${authorEmail ? ` <${authorEmail}>` : ''}`))
642
+ }
643
+
644
+ if (agent.tags && agent.tags.length > 0) {
645
+ console.log(chalk.bold('\n🏷️ Tags:'))
646
+ const tags = Array.isArray(agent.tags) ? agent.tags : Object.values(agent.tags)
647
+ console.log(chalk.gray(` ${tags.join(', ')}`))
648
+ }
649
+
650
+ if (agent.capabilities) {
651
+ console.log(chalk.bold('\n⚡ Capabilities:'))
652
+ const capabilities = Array.isArray(agent.capabilities)
653
+ ? agent.capabilities
654
+ : Object.values(agent.capabilities)
655
+ capabilities.slice(0, 10).forEach(cap => {
656
+ console.log(chalk.gray(` • ${cap}`))
657
+ })
658
+ if (capabilities.length > 10) {
659
+ console.log(chalk.gray(` ... and ${capabilities.length - 10} more`))
660
+ }
661
+ }
662
+
663
+ if (agent.total_installs !== undefined) {
664
+ console.log(chalk.bold('\n📊 Stats:'))
665
+ console.log(chalk.gray(` Installs: ${agent.total_installs}`))
666
+ console.log(chalk.gray(` Rating: ${''.repeat(Math.floor(agent.average_rating || 0))} (${agent.average_rating || 0}/5)`))
667
+ if (agent.total_stars) {
668
+ console.log(chalk.gray(` Stars: ${agent.total_stars}`))
669
+ }
670
+ }
671
+
672
+ if (agent.dependencies?.agents && agent.dependencies.agents.length > 0) {
673
+ console.log(chalk.bold('\n🔗 Dependencies:'))
674
+ agent.dependencies.agents.forEach(dep => {
675
+ console.log(chalk.gray(` • ${dep}`))
676
+ })
677
+ }
678
+
679
+ console.log(chalk.bold('\n🔧 Installation:'))
680
+ console.log(chalk.cyan(` nex agent install ${agent.agent_id || agent.id}`))
681
+
682
+ if (agent.repository_url || agent.links?.repository) {
683
+ console.log(chalk.bold('\n🌐 Links:'))
684
+ const repoUrl = agent.repository_url || agent.links?.repository
685
+ console.log(chalk.gray(` Repository: ${repoUrl}`))
686
+
687
+ if (agent.documentation_url || agent.links?.documentation) {
688
+ const docUrl = agent.documentation_url || agent.links?.documentation
689
+ console.log(chalk.gray(` Documentation: ${docUrl}`))
690
+ }
691
+ }
692
+
693
+ console.log('\n' + chalk.bold.cyan('═'.repeat(60)) + '\n')
694
+ }
695
+
696
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
697
+ // INSTALLATION
698
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
699
+
700
+ /**
701
+ * Install an agent
702
+ */
703
+ async install(agentId, options = {}) {
704
+ const spinner = ora(`Installing ${agentId}...`).start()
705
+
706
+ try {
707
+ // 1. Check if agent is registered in NEX Hub (if Supabase is configured)
708
+ if (this.supabase && !options.skipRegistrationCheck) {
709
+ spinner.text = 'Checking registration...'
710
+
711
+ const { data: { user } } = await this.supabase.auth.getUser()
712
+
713
+ if (user) {
714
+ // User is authenticated, check registration
715
+ const isRegistered = await this.checkAgentRegistration(agentId)
716
+
717
+ if (!isRegistered) {
718
+ spinner.fail('Agent not registered')
719
+ console.log(chalk.yellow(`\n⚠️ Agent "${agentId}" is not registered in your NEX Hub account.\n`))
720
+ console.log(chalk.cyan('💡 Register it first:'))
721
+ console.log(chalk.cyan(` nex agent register ${agentId}\n`))
722
+ console.log(chalk.gray(' Or browse available agents:'))
723
+ console.log(chalk.gray(` nex agent search ${agentId}\n`))
724
+ return false
725
+ }
726
+
727
+ spinner.text = `Installing ${agentId}...`
728
+ } else {
729
+ // User not authenticated, show warning but allow local install
730
+ spinner.warn('Not authenticated - installing locally only')
731
+ console.log(chalk.yellow('\n⚠️ You are not logged in to NEX Hub.'))
732
+ console.log(chalk.gray(' This agent will be installed locally only.\n'))
733
+ }
734
+ }
735
+
736
+ // 2. Resolve version
737
+ const version = options.version || await this.getLatestVersion(agentId)
738
+ spinner.text = `Installing ${agentId}@${version}...`
739
+
740
+ // 3. Load agent manifest
741
+ const manifest = await this.loadLocalManifest(agentId)
742
+
743
+ if (!manifest) {
744
+ spinner.fail(`Agent ${agentId} not found in registry`)
745
+ return false
746
+ }
747
+
748
+ // 4. Check dependencies
749
+ spinner.text = 'Checking dependencies...'
750
+ await this.checkDependencies(manifest)
751
+
752
+ // 5. Determine install location
753
+ const installPath = path.join(this.installPath, agentId)
754
+
755
+ // 5. Check if already installed
756
+ if (await fs.pathExists(installPath)) {
757
+ spinner.info(`Agent ${agentId} already installed`)
758
+
759
+ // Ask user if they want to update
760
+ return await this.update(agentId, options)
761
+ }
762
+
763
+ // 6. Install (symlink or copy)
764
+ const method = options.method || this.config.defaults.method || 'symlink'
765
+ const sourcePath = await this.getAgentSourcePath(agentId, version)
766
+
767
+ spinner.text = `Installing ${agentId} (${method})...`
768
+
769
+ if (method === 'symlink') {
770
+ await fs.ensureDir(path.dirname(installPath))
771
+ await fs.ensureSymlink(sourcePath, installPath)
772
+ } else {
773
+ await fs.copy(sourcePath, installPath)
774
+ }
775
+
776
+ // 7. Track installation
777
+ await this.trackInstallation(agentId, version, method)
778
+
779
+ spinner.succeed(chalk.green(`✅ ${manifest.icon} ${manifest.name} v${version} installed!`))
780
+
781
+ // 8. Show quick start
782
+ console.log(chalk.cyan('\n📖 Quick Start:'))
783
+ console.log(chalk.gray(` @${agentId}`))
784
+ console.log(chalk.gray(` nex agent run ${agentId} <command>\n`))
785
+
786
+ return true
787
+
788
+ } catch (error) {
789
+ spinner.fail(chalk.red(`Failed to install ${agentId}`))
790
+ console.error(chalk.red(`Error: ${error.message}`))
791
+ throw error
792
+ }
793
+ }
794
+
795
+ /**
796
+ * Get agent source path
797
+ */
798
+ async getAgentSourcePath(agentId, version) {
799
+ const categories = ['planning', 'execution', 'community']
800
+
801
+ for (const category of categories) {
802
+ // Try versioned path first
803
+ let sourcePath = path.join(this.registryPath, category, agentId, 'versions', version)
804
+
805
+ if (await fs.pathExists(sourcePath)) {
806
+ return sourcePath
807
+ }
808
+
809
+ // Fallback to non-versioned
810
+ sourcePath = path.join(this.registryPath, category, agentId)
811
+
812
+ if (await fs.pathExists(sourcePath)) {
813
+ return sourcePath
814
+ }
815
+ }
816
+
817
+ throw new Error(`Agent source not found: ${agentId}@${version}`)
818
+ }
819
+
820
+ /**
821
+ * Get latest version of agent
822
+ */
823
+ async getLatestVersion(agentId) {
824
+ // Try Supabase first
825
+ if (this.supabase) {
826
+ const { data } = await this.supabase
827
+ .from('nex_marketplace_versions')
828
+ .select('version')
829
+ .eq('agent_id', agentId)
830
+ .eq('is_latest', true)
831
+ .single()
832
+
833
+ if (data) return data.version
834
+ }
835
+
836
+ // Fallback to manifest
837
+ const manifest = await this.loadLocalManifest(agentId)
838
+ return manifest?.version || manifest?.current_version || '1.0.0'
839
+ }
840
+
841
+ /**
842
+ * Check agent dependencies
843
+ */
844
+ async checkDependencies(manifest) {
845
+ if (!manifest.dependencies || !manifest.dependencies.agents) {
846
+ return true
847
+ }
848
+
849
+ const missing = []
850
+
851
+ for (const depId of manifest.dependencies.agents) {
852
+ const depPath = path.join(this.installPath, depId)
853
+
854
+ if (!await fs.pathExists(depPath)) {
855
+ missing.push(depId)
856
+ }
857
+ }
858
+
859
+ if (missing.length > 0) {
860
+ console.log(chalk.yellow(`\n⚠️ Missing dependencies: ${missing.join(', ')}`))
861
+ console.log(chalk.gray('These agents will be installed automatically.\n'))
862
+
863
+ // Auto-install dependencies
864
+ for (const depId of missing) {
865
+ await this.install(depId)
866
+ }
867
+ }
868
+
869
+ return true
870
+ }
871
+
872
+ /**
873
+ * Track installation
874
+ */
875
+ async trackInstallation(agentId, version, method) {
876
+ // Save to local registry
877
+ const installedFile = path.join(this.installPath, 'installed.json')
878
+ await fs.ensureDir(this.installPath)
879
+
880
+ let installed = {}
881
+ if (await fs.pathExists(installedFile)) {
882
+ installed = await fs.readJSON(installedFile)
883
+ }
884
+
885
+ if (!installed.agents) {
886
+ installed.agents = []
887
+ }
888
+
889
+ // Remove old entry if exists
890
+ installed.agents = installed.agents.filter(a => a.id !== agentId)
891
+
892
+ // Add new entry
893
+ installed.agents.push({
894
+ id: agentId,
895
+ version: version,
896
+ method: method,
897
+ installed_at: new Date().toISOString(),
898
+ project_path: this.projectRoot
899
+ })
900
+
901
+ await fs.writeJSON(installedFile, installed, { spaces: 2 })
902
+
903
+ // Track in Supabase if available
904
+ if (this.supabase) {
905
+ await this.supabase
906
+ .from('nex_marketplace_installs')
907
+ .upsert({
908
+ agent_id: agentId,
909
+ version: version,
910
+ install_method: method,
911
+ project_path: this.projectRoot,
912
+ project_name: path.basename(this.projectRoot),
913
+ is_active: true
914
+ })
915
+ }
916
+ }
917
+
918
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
919
+ // LIST INSTALLED
920
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
921
+
922
+ /**
923
+ * List installed agents
924
+ */
925
+ async list(options = {}) {
926
+ const installedFile = path.join(this.installPath, 'installed.json')
927
+
928
+ if (!await fs.pathExists(installedFile)) {
929
+ console.log(chalk.yellow('📭 No agents installed in this project.'))
930
+ console.log(chalk.gray('\nInstall agents with: nex agent install <agent-id>\n'))
931
+ return []
932
+ }
933
+
934
+ const installed = await fs.readJSON(installedFile)
935
+ const agents = installed.agents || []
936
+
937
+ if (agents.length === 0) {
938
+ console.log(chalk.yellow('📭 No agents installed in this project.\n'))
939
+ return []
940
+ }
941
+
942
+ console.log(chalk.bold.cyan(`\n📦 Installed Agents (${agents.length})`))
943
+ console.log(chalk.gray(`Project: ${path.basename(this.projectRoot)}\n`))
944
+
945
+ for (const agent of agents) {
946
+ const manifest = await this.loadLocalManifest(agent.id)
947
+
948
+ if (manifest) {
949
+ const icon = manifest.icon || '🤖'
950
+ console.log(chalk.bold(`${icon} ${manifest.name || agent.id}`))
951
+ } else {
952
+ console.log(chalk.bold(`🤖 ${agent.id}`))
953
+ }
954
+
955
+ console.log(chalk.gray(` Version: ${agent.version}`))
956
+ console.log(chalk.gray(` Method: ${agent.method}`))
957
+ console.log(chalk.gray(` Installed: ${new Date(agent.installed_at).toLocaleDateString()}`))
958
+ console.log()
959
+ }
960
+
961
+ return agents
962
+ }
963
+
964
+ /**
965
+ * Uninstall agent
966
+ */
967
+ async uninstall(agentId) {
968
+ const spinner = ora(`Uninstalling ${agentId}...`).start()
969
+
970
+ try {
971
+ const installPath = path.join(this.installPath, agentId)
972
+
973
+ if (!await fs.pathExists(installPath)) {
974
+ spinner.fail(`Agent ${agentId} is not installed`)
975
+ return false
976
+ }
977
+
978
+ // Remove files
979
+ await fs.remove(installPath)
980
+
981
+ // Update installed.json
982
+ const installedFile = path.join(this.installPath, 'installed.json')
983
+ const installed = await fs.readJSON(installedFile)
984
+ installed.agents = (installed.agents || []).filter(a => a.id !== agentId)
985
+ await fs.writeJSON(installedFile, installed, { spaces: 2 })
986
+
987
+ // Update Supabase
988
+ if (this.supabase) {
989
+ await this.supabase
990
+ .from('nex_marketplace_installs')
991
+ .update({ is_active: false, uninstalled_at: new Date().toISOString() })
992
+ .eq('agent_id', agentId)
993
+ .eq('project_path', this.projectRoot)
994
+ }
995
+
996
+ spinner.succeed(chalk.green(`✅ Agent ${agentId} uninstalled`))
997
+ return true
998
+
999
+ } catch (error) {
1000
+ spinner.fail(`Failed to uninstall ${agentId}`)
1001
+ console.error(chalk.red(`Error: ${error.message}`))
1002
+ throw error
1003
+ }
1004
+ }
1005
+
1006
+ /**
1007
+ * Update agent
1008
+ */
1009
+ async update(agentId, options = {}) {
1010
+ console.log(chalk.blue(`🔄 Updating ${agentId}...`))
1011
+
1012
+ // Uninstall old version
1013
+ await this.uninstall(agentId)
1014
+
1015
+ // Install new version
1016
+ return await this.install(agentId, options)
1017
+ }
1018
+
1019
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1020
+ // AGENT REGISTRATION (HUB REGISTRY SYSTEM)
1021
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1022
+
1023
+ /**
1024
+ * Register an agent in the NEX Hub
1025
+ */
1026
+ async register(agentId) {
1027
+ const spinner = ora(`Registering ${agentId} in NEX Hub...`).start()
1028
+
1029
+ try {
1030
+ // 1. Check if Supabase is configured
1031
+ if (!this.supabase) {
1032
+ spinner.fail('Supabase not configured')
1033
+ console.log(chalk.yellow('\n⚠️ NEX Hub requires Supabase configuration.'))
1034
+ console.log(chalk.cyan('💡 Set VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY environment variables.\n'))
1035
+ return false
1036
+ }
1037
+
1038
+ // 2. Check if user is authenticated
1039
+ const { data: { user }, error: authError } = await this.supabase.auth.getUser()
1040
+
1041
+ if (authError || !user) {
1042
+ spinner.fail('Not authenticated')
1043
+ console.log(chalk.yellow('\n⚠️ You need to be logged in to register agents.'))
1044
+ console.log(chalk.cyan('💡 Run: nex auth login\n'))
1045
+ return false
1046
+ }
1047
+
1048
+ // 3. Get agent info from database
1049
+ const { data: agent, error: agentError } = await this.supabase
1050
+ .from('nex_marketplace_agents')
1051
+ .select('agent_id, name, agent_type, icon')
1052
+ .eq('agent_id', agentId)
1053
+ .single()
1054
+
1055
+ if (agentError || !agent) {
1056
+ spinner.fail(`Agent ${agentId} not found`)
1057
+ console.log(chalk.yellow(`\n⚠️ Agent "${agentId}" not found in NEX Hub.`))
1058
+ console.log(chalk.cyan(`💡 Search for agents: nex agent search ${agentId}\n`))
1059
+ return false
1060
+ }
1061
+
1062
+ // 4. Call Edge Function to register
1063
+ const EDGE_FUNCTION_URL = process.env.VITE_SUPABASE_URL?.replace('.supabase.co', '.supabase.co/functions/v1')
1064
+ const { data: session } = await this.supabase.auth.getSession()
1065
+
1066
+ const response = await fetch(`${EDGE_FUNCTION_URL}/nex-agent-registry/register`, {
1067
+ method: 'POST',
1068
+ headers: {
1069
+ 'Content-Type': 'application/json',
1070
+ 'Authorization': `Bearer ${session.session?.access_token}`,
1071
+ 'apikey': process.env.VITE_SUPABASE_ANON_KEY
1072
+ },
1073
+ body: JSON.stringify({ agent_id: agentId })
1074
+ })
1075
+
1076
+ const result = await response.json()
1077
+
1078
+ if (!response.ok) {
1079
+ spinner.fail('Registration failed')
1080
+
1081
+ if (result.details?.reason === 'limit_reached') {
1082
+ console.log(chalk.red(`\n❌ ${result.error}\n`))
1083
+ console.log(chalk.yellow(`📊 Your Plan: ${result.details.current_plan}`))
1084
+ console.log(chalk.yellow(` Agent Type: ${result.details.agent_type}`))
1085
+ console.log(chalk.yellow(` Current: ${result.details.current_count}/${result.details.max_allowed}\n`))
1086
+
1087
+ if (result.details.upgrade_required) {
1088
+ console.log(chalk.cyan(`💡 Upgrade to ${result.details.upgrade_required} plan to register more agents.`))
1089
+ console.log(chalk.cyan(` Visit: https://nexhub.dev/pricing\n`))
1090
+ }
1091
+ } else if (result.details?.reason === 'already_registered') {
1092
+ spinner.info('Already registered')
1093
+ console.log(chalk.blue(`\nℹ️ Agent "${agent.name}" is already registered.\n`))
1094
+ return true
1095
+ } else {
1096
+ console.log(chalk.red(`\n❌ ${result.error}\n`))
1097
+ }
1098
+
1099
+ return false
1100
+ }
1101
+
1102
+ // 5. Success!
1103
+ spinner.succeed(chalk.green(`✅ Agent registered successfully!`))
1104
+ console.log(chalk.cyan(`\n${agent.icon || '🤖'} ${agent.name} (${agentId})`))
1105
+ console.log(chalk.gray(` Type: ${agent.agent_type}`))
1106
+ console.log(chalk.gray(` Registration ID: ${result.registration?.registration_id}\n`))
1107
+ console.log(chalk.green(`✨ You can now install this agent:`))
1108
+ console.log(chalk.cyan(` nex agent install ${agentId}\n`))
1109
+
1110
+ return true
1111
+
1112
+ } catch (error) {
1113
+ spinner.fail('Registration failed')
1114
+ console.error(chalk.red(`\n❌ Error: ${error.message}\n`))
1115
+ return false
1116
+ }
1117
+ }
1118
+
1119
+ /**
1120
+ * List registered agents from NEX Hub
1121
+ */
1122
+ async listRegisteredAgents() {
1123
+ const spinner = ora('Loading your registered agents...').start()
1124
+
1125
+ try {
1126
+ // 1. Check if Supabase is configured
1127
+ if (!this.supabase) {
1128
+ spinner.fail('Supabase not configured')
1129
+ console.log(chalk.yellow('\n⚠️ NEX Hub requires Supabase configuration.\n'))
1130
+ return []
1131
+ }
1132
+
1133
+ // 2. Check if user is authenticated
1134
+ const { data: { user }, error: authError } = await this.supabase.auth.getUser()
1135
+
1136
+ if (authError || !user) {
1137
+ spinner.fail('Not authenticated')
1138
+ console.log(chalk.yellow('\n⚠️ You need to be logged in.\n'))
1139
+ return []
1140
+ }
1141
+
1142
+ // 3. Call Edge Function to get registered agents
1143
+ const EDGE_FUNCTION_URL = process.env.VITE_SUPABASE_URL?.replace('.supabase.co', '.supabase.co/functions/v1')
1144
+ const { data: session } = await this.supabase.auth.getSession()
1145
+
1146
+ const response = await fetch(`${EDGE_FUNCTION_URL}/nex-agent-registry/my-agents`, {
1147
+ method: 'GET',
1148
+ headers: {
1149
+ 'Authorization': `Bearer ${session.session?.access_token}`,
1150
+ 'apikey': process.env.VITE_SUPABASE_ANON_KEY
1151
+ }
1152
+ })
1153
+
1154
+ const result = await response.json()
1155
+
1156
+ if (!response.ok) {
1157
+ spinner.fail('Failed to load agents')
1158
+ console.log(chalk.red(`\n❌ ${result.error}\n`))
1159
+ return []
1160
+ }
1161
+
1162
+ const agents = result.agents || []
1163
+
1164
+ spinner.succeed(`Found ${agents.length} registered agents`)
1165
+
1166
+ // 4. Display agents
1167
+ if (agents.length === 0) {
1168
+ console.log(chalk.yellow('\n😕 You have no agents registered yet.\n'))
1169
+ console.log(chalk.cyan('💡 Register an agent:'))
1170
+ console.log(chalk.cyan(' nex agent search <query>'))
1171
+ console.log(chalk.cyan(' nex agent register <agent-id>\n'))
1172
+ return []
1173
+ }
1174
+
1175
+ console.log('\n')
1176
+ console.log(chalk.bold.cyan('📦 YOUR REGISTERED AGENTS\n'))
1177
+
1178
+ agents.forEach(reg => {
1179
+ const agent = reg.nex_marketplace_agents
1180
+ const icon = agent?.icon || '🤖'
1181
+ const name = agent?.name || reg.agent_id
1182
+ const type = agent?.agent_type || 'unknown'
1183
+ const registeredAt = new Date(reg.registered_at).toLocaleDateString()
1184
+
1185
+ console.log(chalk.bold(`${icon} ${name}`))
1186
+ console.log(chalk.gray(` ID: ${reg.agent_id}`))
1187
+ console.log(chalk.gray(` Type: ${type}`))
1188
+ console.log(chalk.gray(` Registered: ${registeredAt}`))
1189
+ console.log(chalk.dim(` ${chalk.cyan('nex agent install')} ${reg.agent_id}`))
1190
+ console.log()
1191
+ })
1192
+
1193
+ // 5. Show plan info
1194
+ const { data: planData } = await this.supabase.rpc('get_user_plan', { p_user_id: user.id })
1195
+
1196
+ if (planData && planData.length > 0) {
1197
+ const plan = planData[0]
1198
+ const nexCount = agents.filter(a => a.nex_marketplace_agents?.agent_type === 'nex_free').length
1199
+ const bmadCount = agents.filter(a => a.nex_marketplace_agents?.agent_type === 'bmad_free').length
1200
+ const premiumCount = agents.filter(a => a.nex_marketplace_agents?.agent_type === 'premium').length
1201
+
1202
+ console.log(chalk.bold.cyan('📊 YOUR PLAN\n'))
1203
+ console.log(chalk.bold(` ${plan.plan_name}`))
1204
+ console.log(chalk.gray(` NEX Free: ${nexCount}/${plan.max_nex_free}`))
1205
+ console.log(chalk.gray(` BMAD Free: ${bmadCount}/${plan.max_bmad_free}`))
1206
+ console.log(chalk.gray(` Premium: ${premiumCount}/${plan.max_premium}`))
1207
+ console.log()
1208
+ }
1209
+
1210
+ return agents
1211
+
1212
+ } catch (error) {
1213
+ spinner.fail('Failed to load agents')
1214
+ console.error(chalk.red(`\n❌ Error: ${error.message}\n`))
1215
+ return []
1216
+ }
1217
+ }
1218
+
1219
+ /**
1220
+ * Check if agent is registered (internal method)
1221
+ */
1222
+ async checkAgentRegistration(agentId) {
1223
+ try {
1224
+ if (!this.supabase) return false
1225
+
1226
+ const { data: { user } } = await this.supabase.auth.getUser()
1227
+ if (!user) return false
1228
+
1229
+ const EDGE_FUNCTION_URL = process.env.VITE_SUPABASE_URL?.replace('.supabase.co', '.supabase.co/functions/v1')
1230
+ const { data: session } = await this.supabase.auth.getSession()
1231
+
1232
+ const response = await fetch(`${EDGE_FUNCTION_URL}/nex-agent-registry/check/${agentId}`, {
1233
+ method: 'GET',
1234
+ headers: {
1235
+ 'Authorization': `Bearer ${session.session?.access_token}`,
1236
+ 'apikey': process.env.VITE_SUPABASE_ANON_KEY
1237
+ }
1238
+ })
1239
+
1240
+ const result = await response.json()
1241
+ return result.is_registered || false
1242
+
1243
+ } catch (error) {
1244
+ return false
1245
+ }
1246
+ }
1247
+ }