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)

Changelog-Fixed: reckless search now returns partial matches instead of requiring exact plugin names.
This commit is contained in:
Tatiana Moroz 2026-01-26 17:29:12 -05:00 committed by Alex Myers
parent ff34e485bc
commit 0173610527
2 changed files with 81 additions and 9 deletions

View file

@ -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)

View file

@ -1548,6 +1548,32 @@ 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
plugins.append((root.name, src))
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 +1589,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 +1615,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