From 90c5f07be44655d9db871b655e8a6c3c614b9086 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Wed, 29 Oct 2025 01:18:28 +0000 Subject: [PATCH] feat(scripts): Add fuzzy search and type filtering for community scripts; improve scripts CLI output and cache handling --- changelog.md | 9 ++++++ deno.json | 1 + ts/00_commitinfo_data.ts | 2 +- ts/moxytool.classes.scriptindex.ts | 47 +++++++++++++++++++++------ ts/moxytool.cli.ts | 51 +++++++++++++++++++----------- ts/moxytool.plugins.ts | 2 ++ 6 files changed, 82 insertions(+), 30 deletions(-) diff --git a/changelog.md b/changelog.md index 838dc16..eb7838c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2025-10-29 - 1.5.0 - feat(scripts) +Add fuzzy search and type filtering for community scripts; improve scripts CLI output and cache handling + +- Integrate @push.rocks/smartfuzzy and use FuzzyMatcher to provide ranked, fuzzy search results for scripts +- Add optional type filtering to scripts search (e.g. type:vm, type:ct, type:pve) and parse a --filter option in the CLI +- Improve scripts CLI output: colored type badges, truncated descriptions, clearer usage/help text and filtered result messaging +- Optimize ScriptIndex.loadCache to avoid reloading when cache is already present +- Update deno.json to include the smartfuzzy dependency and export/import smartfuzzy in plugins + ## 2025-10-28 - 1.4.2 - fix(scriptindex) Handle missing script metadata fields in ScriptIndex.search to prevent crashes diff --git a/deno.json b/deno.json index 6dede03..78a288f 100644 --- a/deno.json +++ b/deno.json @@ -49,6 +49,7 @@ "@push.rocks/smartpath": "npm:@push.rocks/smartpath@^5.0.5", "@push.rocks/smartshell": "npm:@push.rocks/smartshell@^3.2.2", "@push.rocks/smartexpect": "npm:@push.rocks/smartexpect@^1.0.15", + "@push.rocks/smartfuzzy": "npm:@push.rocks/smartfuzzy@^2.0.0", "@push.rocks/smartrx": "npm:@push.rocks/smartrx@^3.0.10", "@push.rocks/smartpromise": "npm:@push.rocks/smartpromise@^4.0.0", "@push.rocks/smartstring": "npm:@push.rocks/smartstring@^4.0.0", diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 733f827..32508d6 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/moxytool', - version: '1.4.2', + version: '1.5.0', description: 'Proxmox administration tool for vGPU setup, VM management, and cluster configuration' } diff --git a/ts/moxytool.classes.scriptindex.ts b/ts/moxytool.classes.scriptindex.ts index 0e44f1d..8f6c822 100644 --- a/ts/moxytool.classes.scriptindex.ts +++ b/ts/moxytool.classes.scriptindex.ts @@ -88,6 +88,11 @@ export class ScriptIndex { */ public async loadCache(): Promise { try { + // Don't reload if already cached + if (this.cache) { + return; + } + if (!Deno) { throw new Error('Deno runtime not available'); } @@ -251,23 +256,45 @@ export class ScriptIndex { } /** - * Search scripts by query string + * Search scripts by query string with optional type filter + * @param query - Search query + * @param typeFilter - Optional type filter (e.g., 'vm', 'ct', 'pve') */ - public search(query: string): IScriptMetadata[] { + public search(query: string, typeFilter?: string): IScriptMetadata[] { if (!this.cache) { return []; } - const lowerQuery = query.toLowerCase(); + let scripts = this.cache.scripts; - return this.cache.scripts.filter((script) => { - // Search in name, description, and slug - return ( - (script.name && script.name.toLowerCase().includes(lowerQuery)) || - (script.slug && script.slug.toLowerCase().includes(lowerQuery)) || - (script.description && script.description.toLowerCase().includes(lowerQuery)) - ); + // Apply type filter if provided + if (typeFilter) { + scripts = scripts.filter((script) => script.type === typeFilter); + } + + // Use smartfuzzy for ranking + const fuzzyMatcher = new plugins.smartfuzzy.FuzzyMatcher(); + + const scoredResults = scripts.map((script) => { + // Calculate match scores for each field + const nameScore = script.name ? fuzzyMatcher.match(query, script.name) : 0; + const slugScore = script.slug ? fuzzyMatcher.match(query, script.slug) : 0; + const descScore = script.description ? fuzzyMatcher.match(query, script.description) : 0; + + // Prioritize: slug > name > description + const totalScore = slugScore * 3 + nameScore * 2 + descScore; + + return { + script, + score: totalScore, + }; }); + + // Filter out non-matches and sort by score + return scoredResults + .filter((result) => result.score > 0) + .sort((a, b) => b.score - a.score) + .map((result) => result.script); } /** diff --git a/ts/moxytool.cli.ts b/ts/moxytool.cli.ts index 74462f2..31111b4 100644 --- a/ts/moxytool.cli.ts +++ b/ts/moxytool.cli.ts @@ -140,17 +140,8 @@ export const runCli = async () => { logger.log('info', ''); try { - // Get current version from deno.json - const denoJsonPath = plugins.path.join(paths.packageDir, 'deno.json'); - let currentVersion = '1.1.0'; // fallback - - try { - const denoJsonContent = await Deno.readTextFile(denoJsonPath); - const denoJson = JSON.parse(denoJsonContent); - currentVersion = denoJson.version || currentVersion; - } catch { - // Use fallback version - } + // Get current version from compile-time imported deno.json + const currentVersion = denoConfig.version; // Fetch latest version from Gitea API const apiUrl = 'https://code.foss.global/api/v1/repos/serve.zone/moxytool/releases/latest'; @@ -289,28 +280,50 @@ export const runCli = async () => { if (!query) { logger.log('error', 'Please provide a search query'); - logger.log('info', 'Usage: moxytool scripts search '); + logger.log('info', 'Usage: moxytool scripts search [--filter type:vm]'); + logger.log('info', 'Filters: type:vm, type:ct, type:pve, type:addon'); return; } - const results = scriptIndex.search(query as string); + // Parse filter option + let typeFilter: string | undefined; + if (argvArg.filter) { + const filterString = argvArg.filter as string; + if (filterString.startsWith('type:')) { + typeFilter = filterString.substring(5); + } + } + + const results = scriptIndex.search(query as string, typeFilter); if (results.length === 0) { - logger.log('warn', `No scripts found matching "${query}"`); + const filterMsg = typeFilter ? ` (filtered by type:${typeFilter})` : ''; + logger.log('warn', `No scripts found matching "${query}"${filterMsg}`); return; } - logger.log('info', `Found ${results.length} script(s) matching "${query}":`); + const filterMsg = typeFilter ? ` \x1b[2m(filtered by type:${typeFilter})\x1b[0m` : ''; + logger.log('info', `Found ${results.length} script(s) matching "\x1b[1m${query}\x1b[0m"${filterMsg}:`); logger.log('info', ''); results.forEach(script => { - logger.log('info', `${script.slug} (${script.type})`); - logger.log('info', ` ${script.name}`); - logger.log('info', ` ${script.description.substring(0, 80)}...`); + const slug = script.slug || 'unknown'; + const description = script.description ? script.description.substring(0, 100) : 'No description available'; + + // Type badge with colors + let typeBadge = ''; + if (script.type === 'ct') typeBadge = '\x1b[36m[LXC]\x1b[0m'; + else if (script.type === 'vm') typeBadge = '\x1b[35m[VM]\x1b[0m'; + else if (script.type === 'pve') typeBadge = '\x1b[33m[PVE]\x1b[0m'; + else typeBadge = `\x1b[2m[${script.type}]\x1b[0m`; + + logger.log('info', `\x1b[1m\x1b[36m►\x1b[0m \x1b[1m${slug}\x1b[0m ${typeBadge}`); + logger.log('info', ` \x1b[2m${script.name}\x1b[0m`); + logger.log('info', ` ${description}${script.description && script.description.length > 100 ? '...' : ''}`); logger.log('info', ''); }); - logger.log('info', 'Use "moxytool scripts info " for more details'); + logger.log('info', '\x1b[2mUse "moxytool scripts info " for more details\x1b[0m'); break; } diff --git a/ts/moxytool.plugins.ts b/ts/moxytool.plugins.ts index 85e77fa..3b70231 100644 --- a/ts/moxytool.plugins.ts +++ b/ts/moxytool.plugins.ts @@ -9,6 +9,7 @@ import * as projectinfo from '@push.rocks/projectinfo'; import * as smartcli from '@push.rocks/smartcli'; import * as smartdelay from '@push.rocks/smartdelay'; import * as smartfile from '@push.rocks/smartfile'; +import * as smartfuzzy from '@push.rocks/smartfuzzy'; import * as smartjson from '@push.rocks/smartjson'; import * as smartlog from '@push.rocks/smartlog'; import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local'; @@ -21,6 +22,7 @@ export { smartcli, smartdelay, smartfile, + smartfuzzy, smartjson, smartlog, smartlogDestinationLocal,