From a71f79fbcae017dd9b088629510f1befa257a3ee Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Fri, 27 Feb 2026 20:29:15 -0500 Subject: [PATCH 1/5] feat: Add flb search command to list and search available extensions - Add `flb search [query]` command with alias `list-extensions` - Supports keyword filtering by name, slug, subtitle, description, and tags - Supports category filtering via --category option - Supports --free flag to show only free extensions - Supports --json flag for machine-readable output - Supports --simple flag for tab-separated scripting output - Supports --host option for self-hosted Fleetbase instances - Uses ANSI colour codes for clean terminal output (chalk v5 is ESM-only) - Add extensionsListApi constant pointing to /~registry/v1/extensions - Add displayExtensionsTable() helper for formatted terminal display - Document new command in README.md with full examples --- README.md | 49 +++++++++++++++++++ index.js | 144 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) diff --git a/README.md b/README.md index 0c56e4c..405c534 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ FLB (Fleetbase CLI) is a command-line interface tool designed for managing Fleet - Automatically convert `composer.json` to `package.json` for PHP packages - Scaffold new Fleetbase extensions - Set registry token to a Fleetbase instance +- Search and list available extensions from the registry - Install and Uninstall extensions - Flexible registry configuration @@ -247,6 +248,54 @@ flb scaffold - `-n, --namespace`: The PHP Namespace of the extension to scaffold - `-r, --repo`: The Repository URL of the extension to scaffold +### Searching for Extensions + +Search and list all available extensions from the Fleetbase registry. Supports keyword search, category filtering, and multiple output formats. + +```bash +flb search [query] +``` + +Alias: `flb list-extensions` + +**Options:** +- `[query]`: (Optional) Search keyword to filter by name, slug, subtitle, description, or tags +- `-c, --category `: Filter by category name or slug +- `-f, --free`: Show only free extensions +- `--json`: Output results as raw JSON (useful for scripting) +- `--simple`: Output one extension per line as tab-separated values: `slug`, `name`, `version`, `price` +- `-h, --host `: API host to fetch extensions from (default: `https://api.fleetbase.io`) + +**Examples:** +```bash +# List all available extensions +flb search + +# Search by keyword +flb search routing +flb search "route optimization" + +# Filter by category +flb search --category telematics + +# Show only free extensions +flb search --free + +# Combine filters +flb search --free --category telematics + +# JSON output for scripting +flb search --json +flb search routing --json | jq '.[].slug' + +# Simple tab-separated output +flb search --simple + +# Search against a self-hosted instance +flb search --host https://api.myfleetbase.com +flb search routing --host http://localhost:8000 +``` + ### Installing a Extension To install a extension, use: diff --git a/index.js b/index.js index 771cc18..7147c9b 100755 --- a/index.js +++ b/index.js @@ -16,6 +16,7 @@ const maxBuffer = 1024 * 1024 * 50; // 50MB const defaultRegistry = 'https://registry.fleetbase.io'; const packageLookupApi = 'https://api.fleetbase.io/~registry/v1/lookup'; const bundleUploadApi = 'https://api.fleetbase.io/~registry/v1/bundle-upload'; +const extensionsListApi = 'https://api.fleetbase.io/~registry/v1/extensions'; const starterExtensionRepo = 'https://github.com/fleetbase/starter-extension.git'; function publishPackage (packagePath, registry, options = {}) { @@ -1293,6 +1294,138 @@ function loginCommand (options) { } } +// Helper: ANSI colour utilities (chalk v5 is ESM-only; use raw ANSI codes in this CJS file) +const ansi = { + reset: '\x1b[0m', + bold: '\x1b[1m', + dim: '\x1b[2m', + green: '\x1b[32m', + yellow: '\x1b[33m', + cyan: '\x1b[36m', + white: '\x1b[37m', + brightWhite: '\x1b[97m', + colorize: (code, text) => `${code}${text}\x1b[0m`, +}; + +// Helper: format extension list for terminal display +function displayExtensionsTable(extensions) { + const count = extensions.length; + console.log(ansi.colorize(ansi.bold + ansi.brightWhite, `Found ${count} extension${count !== 1 ? 's' : ''}:\n`)); + + extensions.forEach((ext, index) => { + const price = ext.payment_required + ? ansi.colorize(ansi.yellow, `$${ext.on_sale ? ext.sale_price : ext.price} ${(ext.currency || 'USD').toUpperCase()}`) + : ansi.colorize(ansi.green, 'Free'); + + const installs = ansi.colorize(ansi.dim, `\u2193 ${ext.installs_count ?? 0}`); + const category = ext.category?.name + ? ansi.colorize(ansi.cyan, `[${ext.category.name}]`) + : ''; + const version = ansi.colorize(ansi.dim, `v${ext.version || '?'}`); + const publisher = ext.publisher?.name + ? ansi.colorize(ansi.dim, `by ${ext.publisher.name}`) + : ''; + + console.log(`${ansi.colorize(ansi.bold + ansi.brightWhite, ext.name)} ${version} ${price} ${installs} ${category}`); + console.log(` ${ansi.colorize(ansi.dim, ext.slug)} ${publisher}`); + if (ext.subtitle) { + console.log(` ${ext.subtitle}`); + } + console.log(` ${ansi.colorize(ansi.dim, 'Install:')} flb install ${ext.slug}`); + + if (index < extensions.length - 1) { + console.log(''); + } + }); + + 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')); + console.log(ansi.colorize(ansi.dim, `Use ${ansi.colorize(ansi.white, 'flb install ')} to install an extension.`)); + console.log(ansi.colorize(ansi.dim, `Use ${ansi.colorize(ansi.white, 'flb search --json')} for machine-readable output.\n`)); +} + +// Command: search and list available extensions +async function searchExtensionsCommand(query, options) { + const host = options.host || 'https://api.fleetbase.io'; + const apiHost = host.startsWith('http://') || host.startsWith('https://') + ? host + : `https://${host}`; + const endpoint = `${apiHost}/~registry/v1/extensions`; + + if (!options.json && !options.simple) { + console.log('\n\u{1F50D} Searching Fleetbase Extensions...\n'); + } + + try { + const response = await axios.get(endpoint); + let extensions = response.data; + + if (!Array.isArray(extensions) || extensions.length === 0) { + console.log('No extensions found.'); + return; + } + + // Filter by search query (name, slug, subtitle, description, tags) + if (query) { + const q = query.toLowerCase(); + extensions = extensions.filter(ext => + ext.name?.toLowerCase().includes(q) || + ext.slug?.toLowerCase().includes(q) || + ext.subtitle?.toLowerCase().includes(q) || + ext.description?.toLowerCase().includes(q) || + (Array.isArray(ext.tags) && ext.tags.some(t => t.toLowerCase().includes(q))) + ); + } + + // Filter by category + if (options.category) { + const cat = options.category.toLowerCase(); + extensions = extensions.filter(ext => + ext.category?.slug?.toLowerCase().includes(cat) || + ext.category?.name?.toLowerCase().includes(cat) + ); + } + + // Filter to free only + if (options.free) { + extensions = extensions.filter(ext => !ext.payment_required); + } + + if (extensions.length === 0) { + const qualifier = query || options.category; + console.log(`No extensions found${qualifier ? ` matching "${qualifier}"` : ''}.`); + return; + } + + // JSON output mode + if (options.json) { + console.log(JSON.stringify(extensions, null, 2)); + return; + } + + // Simple one-per-line output mode (for scripting) + if (options.simple) { + extensions.forEach(ext => { + const price = ext.payment_required ? `$${ext.on_sale ? ext.sale_price : ext.price}` : 'free'; + console.log(`${ext.slug}\t${ext.name}\tv${ext.version || '?'}\t${price}`); + }); + return; + } + + // Default: formatted table output + displayExtensionsTable(extensions); + + } catch (error) { + if (error.response) { + console.error(`\nSearch failed: ${error.response.status} ${error.response.statusText}`); + } else if (error.request) { + console.error('\nSearch failed: No response from server. Check your --host or network connection.'); + } else { + console.error(`\nSearch failed: ${error.message}`); + } + process.exit(1); + } +} + program.name('flb').description('CLI tool for managing Fleetbase Extensions').version(`${packageJson.name} ${packageJson.version}`, '-v, --version', 'Output the current version'); program.option('-r, --registry [url]', 'Specify a fleetbase extension repository', defaultRegistry); @@ -1333,6 +1466,17 @@ program await installPackage(packageName, fleetbasePath); }); +program + .command('search [query]') + .alias('list-extensions') + .description('Search and list available Fleetbase extensions') + .option('-c, --category ', 'Filter by category name or slug') + .option('-f, --free', 'Show only free extensions') + .option('--json', 'Output results as raw JSON') + .option('--simple', 'Output one extension per line: slug, name, version, price (for scripting)') + .option('-h, --host ', 'API host to fetch extensions from (default: https://api.fleetbase.io)') + .action(searchExtensionsCommand); + program .command('uninstall [packageName]') .option('-p, --path ', 'Path of the Fleetbase instance to uninstall for') From 3a256c2489f2977cbd3cfb14fa12ea1374758348 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Fri, 27 Feb 2026 20:33:30 -0500 Subject: [PATCH 2/5] fix: Show both install formats in search output (slug and extension_id) --- index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 7147c9b..eb5d8cc 100755 --- a/index.js +++ b/index.js @@ -1331,7 +1331,8 @@ function displayExtensionsTable(extensions) { if (ext.subtitle) { console.log(` ${ext.subtitle}`); } - console.log(` ${ansi.colorize(ansi.dim, 'Install:')} flb install ${ext.slug}`); + const installSlug = ext.publisher?.slug ? `${ext.publisher.slug}/${ext.slug}` : `fleetbase/${ext.slug}`; + console.log(` ${ansi.colorize(ansi.dim, 'Install:')} flb install ${installSlug} ${ansi.colorize(ansi.dim, `or flb install ${ext.id}`)}`); if (index < extensions.length - 1) { console.log(''); @@ -1339,7 +1340,7 @@ function displayExtensionsTable(extensions) { }); 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')); - console.log(ansi.colorize(ansi.dim, `Use ${ansi.colorize(ansi.white, 'flb install ')} to install an extension.`)); + console.log(ansi.colorize(ansi.dim, `Use ${ansi.colorize(ansi.white, 'flb install fleetbase/')} or ${ansi.colorize(ansi.white, 'flb install ')} to install an extension.`)); console.log(ansi.colorize(ansi.dim, `Use ${ansi.colorize(ansi.white, 'flb search --json')} for machine-readable output.\n`)); } From 3800330599701f10cbab7dd28cc97b813f2b0a20 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Fri, 27 Feb 2026 20:34:45 -0500 Subject: [PATCH 3/5] fix: Convert price from cents to dollars in search output --- index.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index eb5d8cc..834a1ff 100755 --- a/index.js +++ b/index.js @@ -1313,8 +1313,10 @@ function displayExtensionsTable(extensions) { console.log(ansi.colorize(ansi.bold + ansi.brightWhite, `Found ${count} extension${count !== 1 ? 's' : ''}:\n`)); extensions.forEach((ext, index) => { + const rawPrice = ext.on_sale ? ext.sale_price : ext.price; + const formattedPrice = (rawPrice / 100).toFixed(2); const price = ext.payment_required - ? ansi.colorize(ansi.yellow, `$${ext.on_sale ? ext.sale_price : ext.price} ${(ext.currency || 'USD').toUpperCase()}`) + ? ansi.colorize(ansi.yellow, `$${formattedPrice} ${(ext.currency || 'USD').toUpperCase()}`) : ansi.colorize(ansi.green, 'Free'); const installs = ansi.colorize(ansi.dim, `\u2193 ${ext.installs_count ?? 0}`); @@ -1406,7 +1408,8 @@ async function searchExtensionsCommand(query, options) { // Simple one-per-line output mode (for scripting) if (options.simple) { extensions.forEach(ext => { - const price = ext.payment_required ? `$${ext.on_sale ? ext.sale_price : ext.price}` : 'free'; + const rawPrice = ext.on_sale ? ext.sale_price : ext.price; + const price = ext.payment_required ? `$${(rawPrice / 100).toFixed(2)}` : 'free'; console.log(`${ext.slug}\t${ext.name}\tv${ext.version || '?'}\t${price}`); }); return; From 61ebf68c6cd0f2dcdd8edc126bbfefadc6d9fbe2 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Fri, 27 Feb 2026 20:43:15 -0500 Subject: [PATCH 4/5] fix: Always use fleetbase/ prefix in install hint --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 834a1ff..66f7397 100755 --- a/index.js +++ b/index.js @@ -1333,7 +1333,7 @@ function displayExtensionsTable(extensions) { if (ext.subtitle) { console.log(` ${ext.subtitle}`); } - const installSlug = ext.publisher?.slug ? `${ext.publisher.slug}/${ext.slug}` : `fleetbase/${ext.slug}`; + const installSlug = `fleetbase/${ext.slug}`; console.log(` ${ansi.colorize(ansi.dim, 'Install:')} flb install ${installSlug} ${ansi.colorize(ansi.dim, `or flb install ${ext.id}`)}`); if (index < extensions.length - 1) { From 3b3a263c7a41501b386312ae20b369189ff32b05 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Sat, 28 Feb 2026 09:47:15 +0800 Subject: [PATCH 5/5] bumped version to v0.0.5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ee418c7..ae12546 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@fleetbase/cli", - "version": "0.0.3", + "version": "0.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@fleetbase/cli", - "version": "0.0.3", + "version": "0.0.4", "license": "AGPL-3.0-or-later", "dependencies": { "axios": "^1.7.3", diff --git a/package.json b/package.json index 963204a..ba8c603 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fleetbase/cli", - "version": "0.0.4", + "version": "0.0.5", "description": "CLI tool for managing Fleetbase Extensions", "repository": "https://github.com/fleetbase/fleetbase", "license": "AGPL-3.0-or-later",