@@ -16,6 +16,7 @@ const maxBuffer = 1024 * 1024 * 50; // 50MB
1616const defaultRegistry = 'https://registry.fleetbase.io' ;
1717const packageLookupApi = 'https://api.fleetbase.io/~registry/v1/lookup' ;
1818const bundleUploadApi = 'https://api.fleetbase.io/~registry/v1/bundle-upload' ;
19+ const extensionsListApi = 'https://api.fleetbase.io/~registry/v1/extensions' ;
1920const starterExtensionRepo = 'https://github.com/fleetbase/starter-extension.git' ;
2021
2122function publishPackage ( packagePath , registry , options = { } ) {
@@ -1293,6 +1294,142 @@ function loginCommand (options) {
12931294 }
12941295}
12951296
1297+ // Helper: ANSI colour utilities (chalk v5 is ESM-only; use raw ANSI codes in this CJS file)
1298+ const ansi = {
1299+ reset : '\x1b[0m' ,
1300+ bold : '\x1b[1m' ,
1301+ dim : '\x1b[2m' ,
1302+ green : '\x1b[32m' ,
1303+ yellow : '\x1b[33m' ,
1304+ cyan : '\x1b[36m' ,
1305+ white : '\x1b[37m' ,
1306+ brightWhite : '\x1b[97m' ,
1307+ colorize : ( code , text ) => `${ code } ${ text } \x1b[0m` ,
1308+ } ;
1309+
1310+ // Helper: format extension list for terminal display
1311+ function displayExtensionsTable ( extensions ) {
1312+ const count = extensions . length ;
1313+ console . log ( ansi . colorize ( ansi . bold + ansi . brightWhite , `Found ${ count } extension${ count !== 1 ? 's' : '' } :\n` ) ) ;
1314+
1315+ extensions . forEach ( ( ext , index ) => {
1316+ const rawPrice = ext . on_sale ? ext . sale_price : ext . price ;
1317+ const formattedPrice = ( rawPrice / 100 ) . toFixed ( 2 ) ;
1318+ const price = ext . payment_required
1319+ ? ansi . colorize ( ansi . yellow , `$${ formattedPrice } ${ ( ext . currency || 'USD' ) . toUpperCase ( ) } ` )
1320+ : ansi . colorize ( ansi . green , 'Free' ) ;
1321+
1322+ const installs = ansi . colorize ( ansi . dim , `\u2193 ${ ext . installs_count ?? 0 } ` ) ;
1323+ const category = ext . category ?. name
1324+ ? ansi . colorize ( ansi . cyan , `[${ ext . category . name } ]` )
1325+ : '' ;
1326+ const version = ansi . colorize ( ansi . dim , `v${ ext . version || '?' } ` ) ;
1327+ const publisher = ext . publisher ?. name
1328+ ? ansi . colorize ( ansi . dim , `by ${ ext . publisher . name } ` )
1329+ : '' ;
1330+
1331+ console . log ( `${ ansi . colorize ( ansi . bold + ansi . brightWhite , ext . name ) } ${ version } ${ price } ${ installs } ${ category } ` ) ;
1332+ console . log ( ` ${ ansi . colorize ( ansi . dim , ext . slug ) } ${ publisher } ` ) ;
1333+ if ( ext . subtitle ) {
1334+ console . log ( ` ${ ext . subtitle } ` ) ;
1335+ }
1336+ const installSlug = `fleetbase/${ ext . slug } ` ;
1337+ console . log ( ` ${ ansi . colorize ( ansi . dim , 'Install:' ) } flb install ${ installSlug } ${ ansi . colorize ( ansi . dim , `or flb install ${ ext . id } ` ) } ` ) ;
1338+
1339+ if ( index < extensions . length - 1 ) {
1340+ console . log ( '' ) ;
1341+ }
1342+ } ) ;
1343+
1344+ console . log ( ansi . colorize ( ansi . dim , '\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500' ) ) ;
1345+ console . log ( ansi . colorize ( ansi . dim , `Use ${ ansi . colorize ( ansi . white , 'flb install fleetbase/<slug>' ) } or ${ ansi . colorize ( ansi . white , 'flb install <extension_id>' ) } to install an extension.` ) ) ;
1346+ console . log ( ansi . colorize ( ansi . dim , `Use ${ ansi . colorize ( ansi . white , 'flb search --json' ) } for machine-readable output.\n` ) ) ;
1347+ }
1348+
1349+ // Command: search and list available extensions
1350+ async function searchExtensionsCommand ( query , options ) {
1351+ const host = options . host || 'https://api.fleetbase.io' ;
1352+ const apiHost = host . startsWith ( 'http://' ) || host . startsWith ( 'https://' )
1353+ ? host
1354+ : `https://${ host } ` ;
1355+ const endpoint = `${ apiHost } /~registry/v1/extensions` ;
1356+
1357+ if ( ! options . json && ! options . simple ) {
1358+ console . log ( '\n\u{1F50D} Searching Fleetbase Extensions...\n' ) ;
1359+ }
1360+
1361+ try {
1362+ const response = await axios . get ( endpoint ) ;
1363+ let extensions = response . data ;
1364+
1365+ if ( ! Array . isArray ( extensions ) || extensions . length === 0 ) {
1366+ console . log ( 'No extensions found.' ) ;
1367+ return ;
1368+ }
1369+
1370+ // Filter by search query (name, slug, subtitle, description, tags)
1371+ if ( query ) {
1372+ const q = query . toLowerCase ( ) ;
1373+ extensions = extensions . filter ( ext =>
1374+ ext . name ?. toLowerCase ( ) . includes ( q ) ||
1375+ ext . slug ?. toLowerCase ( ) . includes ( q ) ||
1376+ ext . subtitle ?. toLowerCase ( ) . includes ( q ) ||
1377+ ext . description ?. toLowerCase ( ) . includes ( q ) ||
1378+ ( Array . isArray ( ext . tags ) && ext . tags . some ( t => t . toLowerCase ( ) . includes ( q ) ) )
1379+ ) ;
1380+ }
1381+
1382+ // Filter by category
1383+ if ( options . category ) {
1384+ const cat = options . category . toLowerCase ( ) ;
1385+ extensions = extensions . filter ( ext =>
1386+ ext . category ?. slug ?. toLowerCase ( ) . includes ( cat ) ||
1387+ ext . category ?. name ?. toLowerCase ( ) . includes ( cat )
1388+ ) ;
1389+ }
1390+
1391+ // Filter to free only
1392+ if ( options . free ) {
1393+ extensions = extensions . filter ( ext => ! ext . payment_required ) ;
1394+ }
1395+
1396+ if ( extensions . length === 0 ) {
1397+ const qualifier = query || options . category ;
1398+ console . log ( `No extensions found${ qualifier ? ` matching "${ qualifier } "` : '' } .` ) ;
1399+ return ;
1400+ }
1401+
1402+ // JSON output mode
1403+ if ( options . json ) {
1404+ console . log ( JSON . stringify ( extensions , null , 2 ) ) ;
1405+ return ;
1406+ }
1407+
1408+ // Simple one-per-line output mode (for scripting)
1409+ if ( options . simple ) {
1410+ extensions . forEach ( ext => {
1411+ const rawPrice = ext . on_sale ? ext . sale_price : ext . price ;
1412+ const price = ext . payment_required ? `$${ ( rawPrice / 100 ) . toFixed ( 2 ) } ` : 'free' ;
1413+ console . log ( `${ ext . slug } \t${ ext . name } \tv${ ext . version || '?' } \t${ price } ` ) ;
1414+ } ) ;
1415+ return ;
1416+ }
1417+
1418+ // Default: formatted table output
1419+ displayExtensionsTable ( extensions ) ;
1420+
1421+ } catch ( error ) {
1422+ if ( error . response ) {
1423+ console . error ( `\nSearch failed: ${ error . response . status } ${ error . response . statusText } ` ) ;
1424+ } else if ( error . request ) {
1425+ console . error ( '\nSearch failed: No response from server. Check your --host or network connection.' ) ;
1426+ } else {
1427+ console . error ( `\nSearch failed: ${ error . message } ` ) ;
1428+ }
1429+ process . exit ( 1 ) ;
1430+ }
1431+ }
1432+
12961433program . name ( 'flb' ) . description ( 'CLI tool for managing Fleetbase Extensions' ) . version ( `${ packageJson . name } ${ packageJson . version } ` , '-v, --version' , 'Output the current version' ) ;
12971434program . option ( '-r, --registry [url]' , 'Specify a fleetbase extension repository' , defaultRegistry ) ;
12981435
@@ -1333,6 +1470,17 @@ program
13331470 await installPackage ( packageName , fleetbasePath ) ;
13341471 } ) ;
13351472
1473+ program
1474+ . command ( 'search [query]' )
1475+ . alias ( 'list-extensions' )
1476+ . description ( 'Search and list available Fleetbase extensions' )
1477+ . option ( '-c, --category <category>' , 'Filter by category name or slug' )
1478+ . option ( '-f, --free' , 'Show only free extensions' )
1479+ . option ( '--json' , 'Output results as raw JSON' )
1480+ . option ( '--simple' , 'Output one extension per line: slug, name, version, price (for scripting)' )
1481+ . option ( '-h, --host <host>' , 'API host to fetch extensions from (default: https://api.fleetbase.io)' )
1482+ . action ( searchExtensionsCommand ) ;
1483+
13361484program
13371485 . command ( 'uninstall [packageName]' )
13381486 . option ( '-p, --path <path>' , 'Path of the Fleetbase instance to uninstall for' )
0 commit comments