nex-framework-cli 1.0.10 → 1.0.13

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