Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ allprojects {
}

repositories {
mavenLocal()
mavenCentral()
maven { url 'https://repo.eclipse.org/content/groups/releases' }
maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
Expand Down
1 change: 1 addition & 0 deletions modules/nextflow/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ dependencies {
api ('org.jsoup:jsoup:1.15.4')
api 'jline:jline:2.9'
api 'org.pf4j:pf4j:3.12.0'
api 'io.nextflow:nextflow-plugin-gradle:1.0.0-beta.6'
api 'dev.failsafe:failsafe:3.1.0'
api 'io.seqera:lib-trace:0.1.0'

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2013-2024, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package nextflow.cli

import org.pf4j.ExtensionPoint

/**
* Extension point interface for plugins that want to register top-level CLI commands.
*
* Plugins implementing this interface can provide commands that appear as first-class
* citizens in the Nextflow CLI, accessible directly as 'nextflow <command>' rather than
* through the legacy 'nextflow plugin <pluginId>:<command>' syntax.
*
* Example:
* - Instead of: nextflow plugin nf-wave:get-container
* - Enable: nextflow wave get-container
*
* @author Edmund Miller <[email protected]>
*/
interface CommandExtensionPoint extends ExtensionPoint {

/**
* Get the primary command name that this plugin registers.
* This will be the top-level command name (e.g., "wave", "launch").
*
* @return The command name that will be available at the top level
*/
String getCommandName()

/**
* Get the command description for help output.
* This description will appear in the main help listing.
*
* @return A brief description of what this command does
*/
String getCommandDescription()

/**
* Get the priority for this command extension.
* Higher priority commands will take precedence in case of name conflicts.
* Built-in commands have priority 1000 by default.
*
* @return Priority value (higher = higher priority), defaults to 100
*/
default int getPriority() { return 100 }

/**
* Create the actual command instance that will handle execution.
* This method is called when the command needs to be instantiated.
*
* @return A CmdBase instance that will handle the command execution
*/
CmdBase createCommand()

}
91 changes: 78 additions & 13 deletions modules/nextflow/src/main/groovy/nextflow/cli/Launcher.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,26 @@ class Launcher {
}

protected void init() {
allCommands = (List<CmdBase>)[
// Initialize built-in commands
allCommands = createBuiltInCommands()

// Add plugin commands dynamically
addPluginCommands()

options = new CliOptions()
jcommander = new JCommander(options)
for( CmdBase cmd : allCommands ) {
cmd.launcher = this;
jcommander.addCommand(cmd.name, cmd, aliases(cmd))
}
jcommander.setProgramName( APP_NAME )
}

/**
* Create the list of built-in commands
*/
private List<CmdBase> createBuiltInCommands() {
final commands = (List<CmdBase>)[
new CmdClean(),
new CmdClone(),
new CmdConsole(),
Expand All @@ -113,20 +132,56 @@ class Launcher {
]

if(SecretsLoader.isEnabled())
allCommands.add(new CmdSecret())
commands.add(new CmdSecret())

// legacy command
final cmdCloud = SpuriousDeps.cmdCloud()
if( cmdCloud )
allCommands.add(cmdCloud)
commands.add(cmdCloud)

options = new CliOptions()
jcommander = new JCommander(options)
for( CmdBase cmd : allCommands ) {
cmd.launcher = this;
jcommander.addCommand(cmd.name, cmd, aliases(cmd))
return commands
}

/**
* Discover and add plugin commands to the command list
*/
private void addPluginCommands() {
log.debug("Starting plugin command discovery...")
try {
// Ensure plugins are initialized for command discovery
PluginCommandDiscovery.ensurePluginsInitialized()

// Discover plugin commands
final pluginCommandMap = PluginCommandDiscovery.getCommandMap()
log.debug("Plugin command map contains ${pluginCommandMap.size()} commands")

// Check for conflicts between built-in and plugin commands
final builtInNames = allCommands.collect { it.name }.toSet()

pluginCommandMap.each { commandName, extensionPoint ->
log.debug("Processing plugin command: ${commandName}")
if (builtInNames.contains(commandName)) {
log.warn("Plugin command '${commandName}' conflicts with built-in command - skipping plugin version")
}
else {
try {
final pluginCommand = extensionPoint.createCommand()
if (pluginCommand) {
allCommands.add(pluginCommand)
log.debug("Added plugin command: ${commandName}")
}
}
catch (Exception e) {
log.warn("Failed to create plugin command '${commandName}': ${e.message}")
}
}
}

log.debug("Discovered ${pluginCommandMap.size()} plugin commands")
}
catch (Exception e) {
log.warn("Plugin command discovery failed: ${e.message}", e)
}
jcommander.setProgramName( APP_NAME )
}

private static final String[] EMPTY = new String[0]
Expand Down Expand Up @@ -420,11 +475,21 @@ class Launcher {

int len = 0
def all = new TreeMap<String,String>()
new ArrayList<CmdBase>(commands).each {
def description = it.getClass().getAnnotation(Parameters)?.commandDescription()
new ArrayList<CmdBase>(commands).each { cmd ->
String description = null

// Check if it's a plugin command with CommandExtensionPoint interface
if (cmd instanceof CommandExtensionPoint) {
description = ((CommandExtensionPoint) cmd).commandDescription
}
else {
// For built-in commands, use the @Parameters annotation
description = cmd.getClass().getAnnotation(Parameters)?.commandDescription()
}

if( description ) {
all[it.name] = description
if( it.name.size()>len ) len = it.name.size()
all[cmd.name] = description
if( cmd.name.size()>len ) len = cmd.name.size()
}
}

Expand Down
148 changes: 148 additions & 0 deletions modules/nextflow/src/main/groovy/nextflow/cli/PluginCommandBase.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright 2013-2024, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package nextflow.cli

import java.nio.file.Paths

import groovy.transform.CompileStatic
import nextflow.Session
import nextflow.config.ConfigBuilder
import org.slf4j.Logger
import org.slf4j.LoggerFactory

/**
* Abstract base class for plugin commands that want to register as top-level CLI commands.
*
* This class provides the foundation for plugin commands to integrate seamlessly with
* Nextflow's CLI system, including session management and configuration handling.
*
* Plugin commands extending this class will be discoverable and executable as first-class
* CLI commands (e.g., 'nextflow wave' instead of 'nextflow plugin nf-wave:command').
*
* @author Edmund Miller <[email protected]>
*/
@CompileStatic
abstract class PluginCommandBase extends CmdBase implements CommandExtensionPoint {

private static Logger log = LoggerFactory.getLogger(PluginCommandBase)

protected Session session
protected String pluginId

/**
* Constructor that requires the plugin ID for context
*
* @param pluginId The ID of the plugin providing this command
*/
PluginCommandBase(String pluginId) {
this.pluginId = pluginId
}

@Override
String getName() {
return getCommandName()
}

@Override
CmdBase createCommand() {
return this
}

@Override
final void run() {
log.debug("Executing plugin command: ${getCommandName()} from plugin: ${pluginId}")

try {
// Create session for plugin command
initializeSession()

// Execute the actual command logic
execute()
}
catch (Exception e) {
log.error("Error executing plugin command ${getCommandName()}: ${e.message}", e)
throw e
}
finally {
// Clean up session
destroySession()
}
}

/**
* Initialize the Nextflow session for the plugin command.
* This mirrors the session creation pattern from PluginAbstractExec.
*/
protected void initializeSession() {
if (!session) {
final config = new ConfigBuilder()
.setOptions(getLauncher().options)
.setBaseDir(Paths.get('.'))
.build()

this.session = new Session(config)
log.debug("Session initialized for plugin command: ${getCommandName()}")
}
}

/**
* Clean up the session when command execution is complete.
*/
protected void destroySession() {
if (session) {
session.destroy()
session = null
log.debug("Session destroyed for plugin command: ${getCommandName()}")
}
}

/**
* Get the current session. Session will be initialized if not already created.
*
* @return The current Nextflow session
*/
protected Session getSession() {
if (!session) {
initializeSession()
}
return session
}

/**
* Get the plugin ID that provides this command
*
* @return The plugin identifier
*/
String getPluginId() {
return pluginId
}

/**
* Abstract method that subclasses must implement to define their command logic.
* This is called within the session lifecycle management.
*/
abstract protected void execute()

/**
* Default priority for plugin commands. Can be overridden by subclasses.
*/
@Override
int getPriority() {
return 100
}

}
Loading
Loading