From e9ceaf258c3ba7653d751c3a2eb639bc4b5a2132 Mon Sep 17 00:00:00 2001 From: evansmj Date: Fri, 5 Dec 2025 16:06:27 -0500 Subject: [PATCH] Fix Reckless search command not finding partial matches The Reckless search command was only returning a result if you searched a perfect match, which is not too helpful. This updates the command so that partial search matches return a result. Before: reckless search bolt Search exhausted all sources reckless search bol Search exhausted all sources reckless search bolt12-pris Search exhausted all sources After: reckless search bolt Plugins matching 'bolt': bolt12-prism (https://github.com/lightningd/plugins) reckless search bol Plugins matching 'bol': bolt12-prism (https://github.com/lightningd/plugins) reckless search bolt12-pris Plugins matching 'bolt12-pris': bolt12-prism (https://github.com/lightningd/plugins) --- tests/test_reckless.py | 26 ++++++++++++++++++ tools/reckless | 62 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/tests/test_reckless.py b/tests/test_reckless.py index 10ade3624795..14954f7ec25f 100644 --- a/tests/test_reckless.py +++ b/tests/test_reckless.py @@ -224,6 +224,32 @@ def test_search(node_factory): assert r.search_stdout('found testplugpass in source: https://github.com/lightningd/plugins') +def test_search_partial_match(node_factory): + """test that partial/substring search returns multiple matches""" + n = get_reckless_node(node_factory) + + # Search for partial name "testplug" - should find all test plugins + r = reckless([f"--network={NETWORK}", "search", "testplug"], dir=n.lightning_dir) + # Should show the "Plugins matching" header + assert r.search_stdout("Plugins matching 'testplug':") + # Should list multiple plugins (all start with "testplug") + assert r.search_stdout('testplugpass') + assert r.search_stdout('testplugfail') + assert r.search_stdout('testplugpyproj') + assert r.search_stdout('testpluguv') + + # Search for "pass" - should find testplugpass + r = reckless([f"--network={NETWORK}", "search", "pass"], dir=n.lightning_dir) + assert r.search_stdout("Plugins matching 'pass':") + assert r.search_stdout('testplugpass') + # Should not find plugins without "pass" in name + assert not r.search_stdout('testplugfail') + + # Search for something that doesn't exist + r = reckless([f"--network={NETWORK}", "search", "nonexistent"], dir=n.lightning_dir) + assert r.search_stdout("Search exhausted all sources") + + def test_install(node_factory): """test search, git clone, and installation to folder.""" n = get_reckless_node(node_factory) diff --git a/tools/reckless b/tools/reckless index 62a917b6e9f5..9978c0161254 100755 --- a/tools/reckless +++ b/tools/reckless @@ -1548,6 +1548,30 @@ def uninstall(plugin_name: str) -> str: return "uninstalled" +def _get_all_plugins_from_source(src: str) -> list: + """Get all plugin directories from a source repository. + Returns a list of (plugin_name, source_url) tuples.""" + plugins = [] + srctype = Source.get_type(src) + if srctype == Source.UNKNOWN: + return plugins + + try: + root = SourceDir(src, srctype=srctype) + root.populate() + except Exception as e: + log.debug(f"Failed to populate source {src}: {e}") + return plugins + + for item in root.contents: + if isinstance(item, SourceDir): + # Skip archive directories + if 'archive' in item.name.lower(): + continue + plugins.append((item.name, src)) + return plugins + + def search(plugin_name: str) -> Union[InstInfo, None]: """searches plugin index for plugin""" ordered_sources = RECKLESS_SOURCES.copy() @@ -1563,6 +1587,22 @@ def search(plugin_name: str) -> Union[InstInfo, None]: if Source.get_type(src) in [Source.DIRECTORY, Source.LOCAL_REPO]: ordered_sources.remove(src) ordered_sources.insert(0, src) + + # First, collect all partial matches to display to user + partial_matches = [] + for source in ordered_sources: + for plugin_name_found, src_url in _get_all_plugins_from_source(source): + if plugin_name.lower() in plugin_name_found.lower(): + partial_matches.append((plugin_name_found, src_url)) + + # Display all partial matches + if partial_matches: + log.info(f"Plugins matching '{plugin_name}':") + for name, src_url in partial_matches: + log.info(f" {name} ({src_url})") + + # Now try exact match for installation purposes + exact_match = None for source in ordered_sources: srctype = Source.get_type(source) if srctype == Source.UNKNOWN: @@ -1573,17 +1613,21 @@ def search(plugin_name: str) -> Union[InstInfo, None]: found = _source_search(plugin_name, source) if found: log.debug(f"{found}, {found.srctype}") - if not found: - continue - log.info(f"found {found.name} in source: {found.source_loc}") - log.debug(f"entry: {found.entry}") - if found.subdir: - log.debug(f'sub-directory: {found.subdir}') + exact_match = found + break + + if exact_match: + log.info(f"found {exact_match.name} in source: {exact_match.source_loc}") + log.debug(f"entry: {exact_match.entry}") + if exact_match.subdir: + log.debug(f'sub-directory: {exact_match.subdir}') global LAST_FOUND # Stashing the search result saves install() a call to _source_search. - LAST_FOUND = found - return str(found.source_loc) - log.info("Search exhausted all sources") + LAST_FOUND = exact_match + return str(exact_match.source_loc) + + if not partial_matches: + log.info("Search exhausted all sources") return None