From 66c12f35418d6e45b12eabb491ab4b06ebb43db6 Mon Sep 17 00:00:00 2001 From: Cristian Ramon-Cortes Date: Fri, 2 Oct 2026 18:50:45 +0200 Subject: [PATCH 1/5] feat(agents): add agents command Platform API 1.175.0 added workspace agents (CRUD, enable/disable), and 1.190.0 replaced their systemPrompt/templateId fields with agentInstructions/agentInstructionsTemplateId, but tw had no command for any of them. tw agents list/view/add/update/delete/enable/disable cover these operations against the 1.190 agent model. Agents are workspace-scoped and gated per organization by the agent configuration feature, so every command takes a required workspace. The update API replaces the whole agent, so update fetches the agent and resends unchanged fields. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/main/java/io/seqera/tower/cli/Tower.java | 2 + .../tower/cli/commands/AbstractApiCmd.java | 5 + .../seqera/tower/cli/commands/AgentsCmd.java | 43 ++++ .../commands/agents/AbstractAgentsCmd.java | 45 ++++ .../tower/cli/commands/agents/AddCmd.java | 62 +++++ .../agents/AgentInstructionsOptions.java | 38 +++ .../cli/commands/agents/AgentRefOptions.java | 36 +++ .../tower/cli/commands/agents/DeleteCmd.java | 47 ++++ .../tower/cli/commands/agents/DisableCmd.java | 47 ++++ .../tower/cli/commands/agents/EnableCmd.java | 47 ++++ .../tower/cli/commands/agents/ListCmd.java | 56 +++++ .../tower/cli/commands/agents/UpdateCmd.java | 71 ++++++ .../tower/cli/commands/agents/ViewCmd.java | 44 ++++ .../cli/responses/agents/AgentAdded.java | 37 +++ .../cli/responses/agents/AgentDeleted.java | 36 +++ .../cli/responses/agents/AgentUpdated.java | 38 +++ .../tower/cli/responses/agents/AgentView.java | 56 +++++ .../cli/responses/agents/AgentsList.java | 69 ++++++ .../tower/cli/agents/AgentsCmdTest.java | 216 ++++++++++++++++++ 19 files changed, 995 insertions(+) create mode 100644 src/main/java/io/seqera/tower/cli/commands/AgentsCmd.java create mode 100644 src/main/java/io/seqera/tower/cli/commands/agents/AbstractAgentsCmd.java create mode 100644 src/main/java/io/seqera/tower/cli/commands/agents/AddCmd.java create mode 100644 src/main/java/io/seqera/tower/cli/commands/agents/AgentInstructionsOptions.java create mode 100644 src/main/java/io/seqera/tower/cli/commands/agents/AgentRefOptions.java create mode 100644 src/main/java/io/seqera/tower/cli/commands/agents/DeleteCmd.java create mode 100644 src/main/java/io/seqera/tower/cli/commands/agents/DisableCmd.java create mode 100644 src/main/java/io/seqera/tower/cli/commands/agents/EnableCmd.java create mode 100644 src/main/java/io/seqera/tower/cli/commands/agents/ListCmd.java create mode 100644 src/main/java/io/seqera/tower/cli/commands/agents/UpdateCmd.java create mode 100644 src/main/java/io/seqera/tower/cli/commands/agents/ViewCmd.java create mode 100644 src/main/java/io/seqera/tower/cli/responses/agents/AgentAdded.java create mode 100644 src/main/java/io/seqera/tower/cli/responses/agents/AgentDeleted.java create mode 100644 src/main/java/io/seqera/tower/cli/responses/agents/AgentUpdated.java create mode 100644 src/main/java/io/seqera/tower/cli/responses/agents/AgentView.java create mode 100644 src/main/java/io/seqera/tower/cli/responses/agents/AgentsList.java create mode 100644 src/test/java/io/seqera/tower/cli/agents/AgentsCmdTest.java diff --git a/src/main/java/io/seqera/tower/cli/Tower.java b/src/main/java/io/seqera/tower/cli/Tower.java index 74a92294..b0e4ca33 100644 --- a/src/main/java/io/seqera/tower/cli/Tower.java +++ b/src/main/java/io/seqera/tower/cli/Tower.java @@ -23,6 +23,7 @@ import io.seqera.tower.cli.commands.AbstractCmd; import io.seqera.tower.cli.commands.AccessTokensCmd; import io.seqera.tower.cli.commands.ActionsCmd; +import io.seqera.tower.cli.commands.AgentsCmd; import io.seqera.tower.cli.commands.AuditLogsCmd; import io.seqera.tower.cli.commands.CollaboratorsCmd; import io.seqera.tower.cli.commands.ComputeEnvsCmd; @@ -63,6 +64,7 @@ subcommands = { AccessTokensCmd.class, ActionsCmd.class, + AgentsCmd.class, AuditLogsCmd.class, CollaboratorsCmd.class, ComputeEnvsCmd.class, diff --git a/src/main/java/io/seqera/tower/cli/commands/AbstractApiCmd.java b/src/main/java/io/seqera/tower/cli/commands/AbstractApiCmd.java index cf52ab0e..6a3b525a 100644 --- a/src/main/java/io/seqera/tower/cli/commands/AbstractApiCmd.java +++ b/src/main/java/io/seqera/tower/cli/commands/AbstractApiCmd.java @@ -20,6 +20,7 @@ import io.seqera.tower.ApiException; import io.seqera.tower.api.ActionsApi; import io.seqera.tower.api.AdminApi; +import io.seqera.tower.api.AgentsApi; import io.seqera.tower.api.AvatarsApi; import io.seqera.tower.api.ComputeEnvsApi; import io.seqera.tower.api.CredentialsApi; @@ -212,6 +213,10 @@ protected AdminApi adminApi() throws ApiException { return new AdminApi(apiClient()); } + protected AgentsApi agentsApi() throws ApiException { + return new AgentsApi(apiClient()); + } + protected AvatarsApi avatarsApi() throws ApiException { return avatarsApi == null ? new AvatarsApi(apiClient()) : avatarsApi; } diff --git a/src/main/java/io/seqera/tower/cli/commands/AgentsCmd.java b/src/main/java/io/seqera/tower/cli/commands/AgentsCmd.java new file mode 100644 index 00000000..67e8f3fb --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/commands/AgentsCmd.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.commands; + +import io.seqera.tower.cli.commands.agents.AddCmd; +import io.seqera.tower.cli.commands.agents.DeleteCmd; +import io.seqera.tower.cli.commands.agents.DisableCmd; +import io.seqera.tower.cli.commands.agents.EnableCmd; +import io.seqera.tower.cli.commands.agents.ListCmd; +import io.seqera.tower.cli.commands.agents.UpdateCmd; +import io.seqera.tower.cli.commands.agents.ViewCmd; +import picocli.CommandLine.Command; + +@Command( + name = "agents", + description = "Manage AI agents. Agents must be enabled for the workspace organization.", + subcommands = { + ListCmd.class, + ViewCmd.class, + AddCmd.class, + UpdateCmd.class, + DeleteCmd.class, + EnableCmd.class, + DisableCmd.class, + } +) +public class AgentsCmd extends AbstractRootCmd { +} diff --git a/src/main/java/io/seqera/tower/cli/commands/agents/AbstractAgentsCmd.java b/src/main/java/io/seqera/tower/cli/commands/agents/AbstractAgentsCmd.java new file mode 100644 index 00000000..9b536d4e --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/commands/agents/AbstractAgentsCmd.java @@ -0,0 +1,45 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.commands.agents; + +import io.seqera.tower.ApiException; +import io.seqera.tower.cli.commands.AbstractApiCmd; +import io.seqera.tower.cli.exceptions.TowerException; +import io.seqera.tower.model.AgentDbDto; +import picocli.CommandLine.Command; + +@Command +public abstract class AbstractAgentsCmd extends AbstractApiCmd { + + // The API caps agent pages at 100 entries + private static final int MAX_PAGE_SIZE = 100; + + protected AgentDbDto fetchAgent(AgentRefOptions.AgentRef ref, Long wspId) throws ApiException { + String agentId = ref.id != null ? ref.id : agentByName(ref.name, wspId).getId(); + return agentsApi().describeAgent(agentId, wspId).getAgent(); + } + + private AgentDbDto agentByName(String name, Long wspId) throws ApiException { + for (AgentDbDto agent : agentsApi().listAgents(wspId, name, MAX_PAGE_SIZE, 0).getAgents()) { + if (name.equals(agent.getName())) { + return agent; + } + } + throw new TowerException(String.format("Unknown agent '%s' at %s workspace", name, workspaceRef(wspId))); + } +} diff --git a/src/main/java/io/seqera/tower/cli/commands/agents/AddCmd.java b/src/main/java/io/seqera/tower/cli/commands/agents/AddCmd.java new file mode 100644 index 00000000..b96d4450 --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/commands/agents/AddCmd.java @@ -0,0 +1,62 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.commands.agents; + +import io.seqera.tower.ApiException; +import io.seqera.tower.cli.commands.global.WorkspaceRequiredOptions; +import io.seqera.tower.cli.responses.Response; +import io.seqera.tower.cli.responses.agents.AgentAdded; +import io.seqera.tower.model.AgentDbDto; +import io.seqera.tower.model.CreateAgentRequest; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +import java.io.IOException; + +@Command( + name = "add", + description = "Add an agent" +) +public class AddCmd extends AbstractAgentsCmd { + + @Mixin + public WorkspaceRequiredOptions workspace; + + @Option(names = {"-n", "--name"}, description = "Agent name. Must be unique per workspace. Names consist of alphanumeric, hyphen, and underscore characters.", required = true) + public String name; + + @Option(names = {"-d", "--description"}, description = "Agent description (max 120 characters).") + public String description; + + @ArgGroup(multiplicity = "1") + public AgentInstructionsOptions instructions; + + @Override + protected Response exec() throws ApiException, IOException { + Long wspId = workspaceId(workspace.workspace); + CreateAgentRequest request = new CreateAgentRequest() + .name(name) + .description(description) + .agentInstructions(instructions.read()); + + AgentDbDto agent = agentsApi().createAgent(request, wspId).getAgent(); + return new AgentAdded(workspaceRef(wspId), agent); + } +} diff --git a/src/main/java/io/seqera/tower/cli/commands/agents/AgentInstructionsOptions.java b/src/main/java/io/seqera/tower/cli/commands/agents/AgentInstructionsOptions.java new file mode 100644 index 00000000..6fe195dd --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/commands/agents/AgentInstructionsOptions.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.commands.agents; + +import picocli.CommandLine.Option; + +import java.io.IOException; +import java.nio.file.Path; + +import static io.seqera.tower.cli.utils.FilesHelper.readStringOrStdin; + +public class AgentInstructionsOptions { + + @Option(names = {"--instructions"}, description = "Agent instructions: the role, behavior, and constraints for the agent.") + public String text; + + @Option(names = {"--instructions-file"}, description = "File containing the agent instructions. Use '-' to read from stdin.") + public Path file; + + public String read() throws IOException { + return text != null ? text : readStringOrStdin(file); + } +} diff --git a/src/main/java/io/seqera/tower/cli/commands/agents/AgentRefOptions.java b/src/main/java/io/seqera/tower/cli/commands/agents/AgentRefOptions.java new file mode 100644 index 00000000..9e3db312 --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/commands/agents/AgentRefOptions.java @@ -0,0 +1,36 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.commands.agents; + +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Option; + +public class AgentRefOptions { + + @ArgGroup(multiplicity = "1") + public AgentRef agent; + + public static class AgentRef { + + @Option(names = {"-i", "--id"}, description = "Agent identifier") + public String id; + + @Option(names = {"-n", "--name"}, description = "Agent name") + public String name; + } +} diff --git a/src/main/java/io/seqera/tower/cli/commands/agents/DeleteCmd.java b/src/main/java/io/seqera/tower/cli/commands/agents/DeleteCmd.java new file mode 100644 index 00000000..10795eee --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/commands/agents/DeleteCmd.java @@ -0,0 +1,47 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.commands.agents; + +import io.seqera.tower.ApiException; +import io.seqera.tower.cli.commands.global.WorkspaceRequiredOptions; +import io.seqera.tower.cli.responses.Response; +import io.seqera.tower.cli.responses.agents.AgentDeleted; +import io.seqera.tower.model.AgentDbDto; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command( + name = "delete", + description = "Delete an agent" +) +public class DeleteCmd extends AbstractAgentsCmd { + + @Mixin + public WorkspaceRequiredOptions workspace; + + @Mixin + AgentRefOptions ref; + + @Override + protected Response exec() throws ApiException { + Long wspId = workspaceId(workspace.workspace); + AgentDbDto agent = fetchAgent(ref.agent, wspId); + agentsApi().deleteAgent(agent.getId(), wspId); + return new AgentDeleted(workspaceRef(wspId), agent.getName()); + } +} diff --git a/src/main/java/io/seqera/tower/cli/commands/agents/DisableCmd.java b/src/main/java/io/seqera/tower/cli/commands/agents/DisableCmd.java new file mode 100644 index 00000000..20c1a1be --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/commands/agents/DisableCmd.java @@ -0,0 +1,47 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.commands.agents; + +import io.seqera.tower.ApiException; +import io.seqera.tower.cli.commands.global.WorkspaceRequiredOptions; +import io.seqera.tower.cli.responses.Response; +import io.seqera.tower.cli.responses.agents.AgentUpdated; +import io.seqera.tower.model.AgentDbDto; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command( + name = "disable", + description = "Disable an active agent. A disabled agent cannot be launched." +) +public class DisableCmd extends AbstractAgentsCmd { + + @Mixin + public WorkspaceRequiredOptions workspace; + + @Mixin + AgentRefOptions ref; + + @Override + protected Response exec() throws ApiException { + Long wspId = workspaceId(workspace.workspace); + AgentDbDto agent = fetchAgent(ref.agent, wspId); + agentsApi().disableAgent(agent.getId(), wspId); + return new AgentUpdated(workspaceRef(wspId), agent.getName(), "disabled"); + } +} diff --git a/src/main/java/io/seqera/tower/cli/commands/agents/EnableCmd.java b/src/main/java/io/seqera/tower/cli/commands/agents/EnableCmd.java new file mode 100644 index 00000000..4c96068c --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/commands/agents/EnableCmd.java @@ -0,0 +1,47 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.commands.agents; + +import io.seqera.tower.ApiException; +import io.seqera.tower.cli.commands.global.WorkspaceRequiredOptions; +import io.seqera.tower.cli.responses.Response; +import io.seqera.tower.cli.responses.agents.AgentUpdated; +import io.seqera.tower.model.AgentDbDto; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command( + name = "enable", + description = "Enable an inactive agent" +) +public class EnableCmd extends AbstractAgentsCmd { + + @Mixin + public WorkspaceRequiredOptions workspace; + + @Mixin + AgentRefOptions ref; + + @Override + protected Response exec() throws ApiException { + Long wspId = workspaceId(workspace.workspace); + AgentDbDto agent = fetchAgent(ref.agent, wspId); + agentsApi().enableAgent(agent.getId(), wspId); + return new AgentUpdated(workspaceRef(wspId), agent.getName(), "enabled"); + } +} diff --git a/src/main/java/io/seqera/tower/cli/commands/agents/ListCmd.java b/src/main/java/io/seqera/tower/cli/commands/agents/ListCmd.java new file mode 100644 index 00000000..f8c7416a --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/commands/agents/ListCmd.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.commands.agents; + +import io.seqera.tower.ApiException; +import io.seqera.tower.cli.commands.global.PaginationOptions; +import io.seqera.tower.cli.commands.global.WorkspaceRequiredOptions; +import io.seqera.tower.cli.responses.Response; +import io.seqera.tower.cli.responses.agents.AgentsList; +import io.seqera.tower.cli.utils.PaginationInfo; +import io.seqera.tower.model.ListAgentsResponse; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +@Command( + name = "list", + description = "List agents" +) +public class ListCmd extends AbstractAgentsCmd { + + @Mixin + public WorkspaceRequiredOptions workspace; + + @Option(names = {"-f", "--filter"}, description = "Filter agents by name") + public String filter; + + @Mixin + PaginationOptions paginationOptions; + + @Override + protected Response exec() throws ApiException { + Long wspId = workspaceId(workspace.workspace); + Integer max = PaginationOptions.getMax(paginationOptions); + Integer offset = PaginationOptions.getOffset(paginationOptions, max); + + ListAgentsResponse response = agentsApi().listAgents(wspId, filter, max, offset); + + return new AgentsList(workspaceRef(wspId), response.getAgents(), PaginationInfo.from(paginationOptions, response.getTotalSize())); + } +} diff --git a/src/main/java/io/seqera/tower/cli/commands/agents/UpdateCmd.java b/src/main/java/io/seqera/tower/cli/commands/agents/UpdateCmd.java new file mode 100644 index 00000000..ad46ec7f --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/commands/agents/UpdateCmd.java @@ -0,0 +1,71 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.commands.agents; + +import io.seqera.tower.ApiException; +import io.seqera.tower.cli.commands.global.WorkspaceRequiredOptions; +import io.seqera.tower.cli.responses.Response; +import io.seqera.tower.cli.responses.agents.AgentUpdated; +import io.seqera.tower.model.AgentDbDto; +import io.seqera.tower.model.UpdateAgentRequest; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +import java.io.IOException; + +import static io.seqera.tower.cli.utils.ModelHelper.coalesce; + +@Command( + name = "update", + description = "Update an agent" +) +public class UpdateCmd extends AbstractAgentsCmd { + + @Mixin + public WorkspaceRequiredOptions workspace; + + @Mixin + AgentRefOptions ref; + + @Option(names = {"--new-name"}, description = "New agent name. Names consist of alphanumeric, hyphen, and underscore characters.") + public String newName; + + @Option(names = {"-d", "--description"}, description = "Agent description (max 120 characters).") + public String description; + + @ArgGroup + public AgentInstructionsOptions instructions; + + @Override + protected Response exec() throws ApiException, IOException { + Long wspId = workspaceId(workspace.workspace); + AgentDbDto agent = fetchAgent(ref.agent, wspId); + + // The API replaces the whole agent, so unchanged fields are sent with their current value + UpdateAgentRequest request = new UpdateAgentRequest() + .name(coalesce(newName, agent.getName())) + .description(coalesce(description, agent.getDescription())) + .agentInstructions(instructions != null ? instructions.read() : agent.getAgentInstructions()) + .agentInstructionsTemplateId(agent.getAgentInstructionsTemplateId()); + + AgentDbDto updated = agentsApi().updateAgent(agent.getId(), request, wspId).getAgent(); + return new AgentUpdated(workspaceRef(wspId), updated.getName(), "updated"); + } +} diff --git a/src/main/java/io/seqera/tower/cli/commands/agents/ViewCmd.java b/src/main/java/io/seqera/tower/cli/commands/agents/ViewCmd.java new file mode 100644 index 00000000..b1605e6e --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/commands/agents/ViewCmd.java @@ -0,0 +1,44 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.commands.agents; + +import io.seqera.tower.ApiException; +import io.seqera.tower.cli.commands.global.WorkspaceRequiredOptions; +import io.seqera.tower.cli.responses.Response; +import io.seqera.tower.cli.responses.agents.AgentView; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +@Command( + name = "view", + description = "View agent details" +) +public class ViewCmd extends AbstractAgentsCmd { + + @Mixin + public WorkspaceRequiredOptions workspace; + + @Mixin + AgentRefOptions ref; + + @Override + protected Response exec() throws ApiException { + Long wspId = workspaceId(workspace.workspace); + return new AgentView(workspaceRef(wspId), fetchAgent(ref.agent, wspId)); + } +} diff --git a/src/main/java/io/seqera/tower/cli/responses/agents/AgentAdded.java b/src/main/java/io/seqera/tower/cli/responses/agents/AgentAdded.java new file mode 100644 index 00000000..4e9d309f --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/responses/agents/AgentAdded.java @@ -0,0 +1,37 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.responses.agents; + +import io.seqera.tower.cli.responses.Response; +import io.seqera.tower.model.AgentDbDto; + +public class AgentAdded extends Response { + + public final String workspaceRef; + public final AgentDbDto agent; + + public AgentAdded(String workspaceRef, AgentDbDto agent) { + this.workspaceRef = workspaceRef; + this.agent = agent; + } + + @Override + public String toString() { + return ansi(String.format("%n @|yellow New agent '%s' (%s) added at %s workspace|@%n", agent.getName(), agent.getId(), workspaceRef)); + } +} diff --git a/src/main/java/io/seqera/tower/cli/responses/agents/AgentDeleted.java b/src/main/java/io/seqera/tower/cli/responses/agents/AgentDeleted.java new file mode 100644 index 00000000..3d912b89 --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/responses/agents/AgentDeleted.java @@ -0,0 +1,36 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.responses.agents; + +import io.seqera.tower.cli.responses.Response; + +public class AgentDeleted extends Response { + + public final String workspaceRef; + public final String agentName; + + public AgentDeleted(String workspaceRef, String agentName) { + this.workspaceRef = workspaceRef; + this.agentName = agentName; + } + + @Override + public String toString() { + return ansi(String.format("%n @|yellow Agent '%s' deleted at %s workspace|@%n", agentName, workspaceRef)); + } +} diff --git a/src/main/java/io/seqera/tower/cli/responses/agents/AgentUpdated.java b/src/main/java/io/seqera/tower/cli/responses/agents/AgentUpdated.java new file mode 100644 index 00000000..c2cb9d7c --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/responses/agents/AgentUpdated.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.responses.agents; + +import io.seqera.tower.cli.responses.Response; + +public class AgentUpdated extends Response { + + public final String workspaceRef; + public final String agentName; + public final String action; + + public AgentUpdated(String workspaceRef, String agentName, String action) { + this.workspaceRef = workspaceRef; + this.agentName = agentName; + this.action = action; + } + + @Override + public String toString() { + return ansi(String.format("%n @|yellow Agent '%s' %s at %s workspace|@%n", agentName, action, workspaceRef)); + } +} diff --git a/src/main/java/io/seqera/tower/cli/responses/agents/AgentView.java b/src/main/java/io/seqera/tower/cli/responses/agents/AgentView.java new file mode 100644 index 00000000..48c85352 --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/responses/agents/AgentView.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.responses.agents; + +import io.seqera.tower.cli.responses.Response; +import io.seqera.tower.cli.utils.TableList; +import io.seqera.tower.model.AgentDbDto; + +import java.io.PrintWriter; +import java.util.Objects; + +import static io.seqera.tower.cli.utils.FormatHelper.formatDate; + +public class AgentView extends Response { + + public final String workspaceRef; + public final AgentDbDto agent; + + public AgentView(String workspaceRef, AgentDbDto agent) { + this.workspaceRef = workspaceRef; + this.agent = agent; + } + + @Override + public void toString(PrintWriter out) { + out.println(ansi(String.format("%n @|bold Agent at %s workspace:|@%n", workspaceRef))); + TableList table = new TableList(out, 2); + table.setPrefix(" "); + table.addRow("ID", agent.getId()); + table.addRow("Name", agent.getName()); + table.addRow("Description", Objects.toString(agent.getDescription(), "")); + table.addRow("Status", agent.getStatus() == null ? "NA" : agent.getStatus().getValue()); + table.addRow("Created", formatDate(agent.getDateCreated())); + table.addRow("Updated", formatDate(agent.getLastUpdated())); + table.print(); + + out.println(ansi(String.format("%n @|bold Instructions:|@%n"))); + out.println(agent.getAgentInstructions()); + out.println(""); + } +} diff --git a/src/main/java/io/seqera/tower/cli/responses/agents/AgentsList.java b/src/main/java/io/seqera/tower/cli/responses/agents/AgentsList.java new file mode 100644 index 00000000..1a5082b4 --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/responses/agents/AgentsList.java @@ -0,0 +1,69 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.responses.agents; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.seqera.tower.cli.responses.Response; +import io.seqera.tower.cli.utils.PaginationInfo; +import io.seqera.tower.cli.utils.TableList; +import io.seqera.tower.model.AgentDbDto; +import jakarta.annotation.Nullable; + +import java.io.PrintWriter; +import java.util.List; + +import static io.seqera.tower.cli.utils.FormatHelper.formatDate; +import static io.seqera.tower.cli.utils.FormatHelper.formatDescription; + +public class AgentsList extends Response { + + public final String workspaceRef; + public final List agents; + + @JsonIgnore + @Nullable + private final PaginationInfo paginationInfo; + + public AgentsList(String workspaceRef, List agents, @Nullable PaginationInfo paginationInfo) { + this.workspaceRef = workspaceRef; + this.agents = agents; + this.paginationInfo = paginationInfo; + } + + @Override + public void toString(PrintWriter out) { + out.println(ansi(String.format("%n @|bold Agents at %s workspace:|@%n", workspaceRef))); + if (agents == null || agents.isEmpty()) { + out.println(ansi(" @|yellow No agents found|@")); + return; + } + + TableList table = new TableList(out, 5, "ID", "Name", "Description", "Status", "Updated"); + table.setPrefix(" "); + agents.forEach(agent -> table.addRow( + agent.getId(), + agent.getName(), + formatDescription(agent.getDescription(), 100), + agent.getStatus() == null ? "NA" : agent.getStatus().getValue(), + formatDate(agent.getLastUpdated()) + )); + table.print(); + PaginationInfo.addFooter(out, paginationInfo); + out.println(""); + } +} diff --git a/src/test/java/io/seqera/tower/cli/agents/AgentsCmdTest.java b/src/test/java/io/seqera/tower/cli/agents/AgentsCmdTest.java new file mode 100644 index 00000000..2d91a3ae --- /dev/null +++ b/src/test/java/io/seqera/tower/cli/agents/AgentsCmdTest.java @@ -0,0 +1,216 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.agents; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.seqera.tower.cli.BaseCmdTest; +import io.seqera.tower.cli.commands.enums.OutputType; +import io.seqera.tower.cli.exceptions.TowerException; +import io.seqera.tower.cli.responses.agents.AgentAdded; +import io.seqera.tower.cli.responses.agents.AgentDeleted; +import io.seqera.tower.cli.responses.agents.AgentUpdated; +import io.seqera.tower.cli.responses.agents.AgentView; +import io.seqera.tower.cli.responses.agents.AgentsList; +import io.seqera.tower.cli.utils.PaginationInfo; +import io.seqera.tower.model.AgentDbDto; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockserver.client.MockServerClient; +import org.mockserver.model.MediaType; + +import java.io.IOException; +import java.util.List; + +import static io.seqera.tower.cli.utils.JsonHelper.parseJson; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockserver.matchers.Times.exactly; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; +import static org.mockserver.model.JsonBody.json; + +class AgentsCmdTest extends BaseCmdTest { + + private static final String WSP_ID = "75887156211589"; + private static final String WSP_REF = "[organization1 / workspace1]"; + + private static final String AGENT = """ + { + "id": "agt_1", + "name": "fix-failed-runs", + "description": "Debug failed runs", + "agentInstructions": "Find the root cause", + "agentInstructionsTemplateId": "debug-template", + "status": "active", + "workspaceId": 75887156211589, + "createdBy": 1264, + "dateCreated": "2026-09-01T10:00:00Z", + "lastUpdated": "2026-09-02T10:00:00Z" + } + """; + + @BeforeEach + void init(MockServerClient mock) { + mock.reset(); + mock.when(request().withMethod("GET").withPath("/user-info"), exactly(1)) + .respond(response().withStatusCode(200).withBody(loadResource("user")).withContentType(MediaType.APPLICATION_JSON)); + mock.when(request().withMethod("GET").withPath("/user/1264/workspaces"), exactly(1)) + .respond(response().withStatusCode(200).withBody(loadResource("workspaces/workspaces_list")).withContentType(MediaType.APPLICATION_JSON)); + } + + private void mockDescribe(MockServerClient mock) { + mock.when(request().withMethod("GET").withPath("/agents/agt_1").withQueryStringParameter("workspaceId", WSP_ID), exactly(1)) + .respond(response().withStatusCode(200).withBody("{\"agent\":" + AGENT + "}").withContentType(MediaType.APPLICATION_JSON)); + } + + private void mockFindByName(MockServerClient mock) { + mock.when(request().withMethod("GET").withPath("/agents") + .withQueryStringParameter("workspaceId", WSP_ID) + .withQueryStringParameter("search", "fix-failed-runs") + .withQueryStringParameter("max", "100") + .withQueryStringParameter("offset", "0"), exactly(1)) + .respond(response().withStatusCode(200).withBody("{\"agents\":[" + AGENT.replace("fix-failed-runs", "fix-failed-runs-v2").replace("agt_1", "agt_2") + "," + AGENT + "],\"totalSize\":2}").withContentType(MediaType.APPLICATION_JSON)); + } + + private static AgentDbDto agent() throws JsonProcessingException { + return parseJson(AGENT, AgentDbDto.class); + } + + @ParameterizedTest + @EnumSource(OutputType.class) + void testList(OutputType format, MockServerClient mock) throws JsonProcessingException { + mock.when(request().withMethod("GET").withPath("/agents") + .withQueryStringParameter("workspaceId", WSP_ID) + .withQueryStringParameter("search", "fix") + .withQueryStringParameter("max", "10") + .withQueryStringParameter("offset", "10"), exactly(1)) + .respond(response().withStatusCode(200).withBody("{\"agents\":[" + AGENT + "],\"totalSize\":11}").withContentType(MediaType.APPLICATION_JSON)); + + ExecOut out = exec(format, mock, "agents", "list", "-w", WSP_ID, "-f", "fix", "--max", "10", "--page", "2"); + + assertOutput(format, out, new AgentsList(WSP_REF, List.of(agent()), PaginationInfo.from(null, 10, 2, 11L))); + } + + @ParameterizedTest + @EnumSource(OutputType.class) + void testViewByName(OutputType format, MockServerClient mock) throws JsonProcessingException { + mockFindByName(mock); + mockDescribe(mock); + + ExecOut out = exec(format, mock, "agents", "view", "-w", WSP_ID, "-n", "fix-failed-runs"); + + assertOutput(format, out, new AgentView(WSP_REF, agent())); + } + + @Test + void testViewUnknownName(MockServerClient mock) { + mock.when(request().withMethod("GET").withPath("/agents").withQueryStringParameter("search", "missing"), exactly(1)) + .respond(response().withStatusCode(200).withBody("{\"agents\":[],\"totalSize\":0}").withContentType(MediaType.APPLICATION_JSON)); + + ExecOut out = exec(mock, "agents", "view", "-w", WSP_ID, "-n", "missing"); + + assertEquals(errorMessage(out.app, new TowerException("Unknown agent 'missing' at " + WSP_REF + " workspace")), out.stdErr); + assertEquals(1, out.exitCode); + } + + @ParameterizedTest + @EnumSource(OutputType.class) + void testAdd(OutputType format, MockServerClient mock) throws IOException { + mock.when(request().withMethod("POST").withPath("/agents").withQueryStringParameter("workspaceId", WSP_ID) + .withBody(json(""" + { + "name": "fix-failed-runs", + "description": "Debug failed runs", + "agentInstructions": "Find the root cause" + } + """)), exactly(1)) + .respond(response().withStatusCode(200).withBody("{\"agent\":" + AGENT + "}").withContentType(MediaType.APPLICATION_JSON)); + + String instructions = tempFile("Find the root cause", "instructions", ".md"); + ExecOut out = exec(format, mock, "agents", "add", "-w", WSP_ID, "-n", "fix-failed-runs", "-d", "Debug failed runs", + "--instructions-file", instructions); + + assertOutput(format, out, new AgentAdded(WSP_REF, agent())); + } + + @Test + void testAddRequiresInstructions(MockServerClient mock) { + ExecOut out = exec(mock, "agents", "add", "-w", WSP_ID, "-n", "fix-failed-runs"); + + assertEquals(2, out.exitCode); + } + + @ParameterizedTest + @EnumSource(OutputType.class) + void testUpdate(OutputType format, MockServerClient mock) throws JsonProcessingException { + mockDescribe(mock); + // Fields not given on the command line keep their current value, as the API replaces the agent + mock.when(request().withMethod("PUT").withPath("/agents/agt_1").withQueryStringParameter("workspaceId", WSP_ID) + .withBody(json(""" + { + "name": "new-name", + "description": "Debug failed runs", + "agentInstructions": "New instructions", + "agentInstructionsTemplateId": "debug-template" + } + """)), exactly(1)) + .respond(response().withStatusCode(200).withBody("{\"agent\":" + AGENT.replace("\"fix-failed-runs\"", "\"new-name\"") + "}").withContentType(MediaType.APPLICATION_JSON)); + + ExecOut out = exec(format, mock, "agents", "update", "-w", WSP_ID, "-i", "agt_1", "--new-name", "new-name", "--instructions", "New instructions"); + + assertOutput(format, out, new AgentUpdated(WSP_REF, "new-name", "updated")); + } + + @ParameterizedTest + @EnumSource(OutputType.class) + void testDelete(OutputType format, MockServerClient mock) { + mockDescribe(mock); + mock.when(request().withMethod("DELETE").withPath("/agents/agt_1").withQueryStringParameter("workspaceId", WSP_ID), exactly(1)) + .respond(response().withStatusCode(204)); + + ExecOut out = exec(format, mock, "agents", "delete", "-w", WSP_ID, "-i", "agt_1"); + + assertOutput(format, out, new AgentDeleted(WSP_REF, "fix-failed-runs")); + } + + @ParameterizedTest + @EnumSource(OutputType.class) + void testEnable(OutputType format, MockServerClient mock) { + mockDescribe(mock); + mock.when(request().withMethod("POST").withPath("/agents/agt_1/enable").withQueryStringParameter("workspaceId", WSP_ID), exactly(1)) + .respond(response().withStatusCode(200).withBody("{\"agent\":" + AGENT + "}").withContentType(MediaType.APPLICATION_JSON)); + + ExecOut out = exec(format, mock, "agents", "enable", "-w", WSP_ID, "-i", "agt_1"); + + assertOutput(format, out, new AgentUpdated(WSP_REF, "fix-failed-runs", "enabled")); + } + + @ParameterizedTest + @EnumSource(OutputType.class) + void testDisable(OutputType format, MockServerClient mock) { + mockFindByName(mock); + mockDescribe(mock); + mock.when(request().withMethod("POST").withPath("/agents/agt_1/disable").withQueryStringParameter("workspaceId", WSP_ID), exactly(1)) + .respond(response().withStatusCode(200).withBody("{\"agent\":" + AGENT.replace("active", "inactive") + "}").withContentType(MediaType.APPLICATION_JSON)); + + ExecOut out = exec(format, mock, "agents", "disable", "-w", WSP_ID, "-n", "fix-failed-runs"); + + assertOutput(format, out, new AgentUpdated(WSP_REF, "fix-failed-runs", "disabled")); + } +} From c6be35bde4fad2bcf43b24a1988fff7dd0f89cce Mon Sep 17 00:00:00 2001 From: Cristian Ramon-Cortes Date: Fri, 2 Oct 2026 18:39:40 +0200 Subject: [PATCH 2/5] feat(info): add 'tw info versions' to list launchable Nextflow versions --nextflow-version must name a version Platform knows, but tw had no way to list them. Platform API 1.190.0 added GET /platform/versions, which returns the configured Nextflow versions and the default one, as the launch form uses them. It lives under 'tw info' because it describes the Platform deployment rather than a workspace resource: 'tw launch' takes a positional pipeline name a subcommand would shadow, and 'tw pipelines versions' already means pipeline versions. The --nextflow-version help now points at it. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../tower/cli/commands/AbstractApiCmd.java | 6 +++ .../io/seqera/tower/cli/commands/InfoCmd.java | 6 ++- .../tower/cli/commands/info/VersionsCmd.java | 45 ++++++++++++++++ .../pipelines/NullableLaunchOptions.java | 2 +- .../cli/responses/ComponentVersions.java | 52 +++++++++++++++++++ .../java/io/seqera/tower/cli/InfoCmdTest.java | 29 +++++++++++ 6 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 src/main/java/io/seqera/tower/cli/commands/info/VersionsCmd.java create mode 100644 src/main/java/io/seqera/tower/cli/responses/ComponentVersions.java diff --git a/src/main/java/io/seqera/tower/cli/commands/AbstractApiCmd.java b/src/main/java/io/seqera/tower/cli/commands/AbstractApiCmd.java index 6a3b525a..d013e389 100644 --- a/src/main/java/io/seqera/tower/cli/commands/AbstractApiCmd.java +++ b/src/main/java/io/seqera/tower/cli/commands/AbstractApiCmd.java @@ -30,6 +30,7 @@ import io.seqera.tower.api.IdentitiesApi; import io.seqera.tower.api.LabelsApi; import io.seqera.tower.api.LaunchApi; +import io.seqera.tower.api.NextflowApi; import io.seqera.tower.api.OrgsApi; import io.seqera.tower.api.PipelineSchemasApi; import io.seqera.tower.api.PipelineSecretsApi; @@ -131,6 +132,7 @@ public abstract class AbstractApiCmd extends AbstractCmd { private IdentitiesApi identitiesApi; private LabelsApi labelsApi; private LaunchApi launchApi; + private NextflowApi nextflowApi; private OrgsApi orgsApi; private PipelinesApi pipelinesApi; private PipelineSchemasApi pipelineSchemasApi; @@ -253,6 +255,10 @@ protected LaunchApi launchApi() throws ApiException { return launchApi == null ? new LaunchApi(apiClient()) : launchApi; } + protected NextflowApi nextflowApi() throws ApiException { + return nextflowApi == null ? new NextflowApi(apiClient()) : nextflowApi; + } + protected OrgsApi orgsApi() throws ApiException { return orgsApi == null ? new OrgsApi(apiClient()) : orgsApi; } diff --git a/src/main/java/io/seqera/tower/cli/commands/InfoCmd.java b/src/main/java/io/seqera/tower/cli/commands/InfoCmd.java index 5b5ab3bb..fece7985 100644 --- a/src/main/java/io/seqera/tower/cli/commands/InfoCmd.java +++ b/src/main/java/io/seqera/tower/cli/commands/InfoCmd.java @@ -17,6 +17,7 @@ package io.seqera.tower.cli.commands; import io.seqera.tower.ApiException; +import io.seqera.tower.cli.commands.info.VersionsCmd; import io.seqera.tower.cli.exceptions.TowerException; import io.seqera.tower.cli.responses.InfoResponse; import io.seqera.tower.cli.responses.Response; @@ -32,7 +33,10 @@ @CommandLine.Command( name = "info", - description = "Show system info and health status" + description = "Show system info and health status", + subcommands = { + VersionsCmd.class, + } ) public class InfoCmd extends AbstractRootCmd { @Override diff --git a/src/main/java/io/seqera/tower/cli/commands/info/VersionsCmd.java b/src/main/java/io/seqera/tower/cli/commands/info/VersionsCmd.java new file mode 100644 index 00000000..a0778ccd --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/commands/info/VersionsCmd.java @@ -0,0 +1,45 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.commands.info; + +import io.seqera.tower.ApiException; +import io.seqera.tower.cli.commands.AbstractApiCmd; +import io.seqera.tower.cli.responses.ComponentVersions; +import io.seqera.tower.cli.responses.ComponentVersions.ComponentVersion; +import io.seqera.tower.cli.responses.Response; +import io.seqera.tower.model.NextflowVersion; +import picocli.CommandLine.Command; + +import java.util.List; +import java.util.Objects; + +@Command( + name = "versions", + description = "List the Nextflow versions available to launch with" +) +public class VersionsCmd extends AbstractApiCmd { + + private static final String NEXTFLOW = "nextflow"; + + @Override + protected Response exec() throws ApiException { + List versions = Objects.requireNonNullElse(nextflowApi().nextflowVersions().getNextflowVersions(), List.of()).stream() + .map(v -> new ComponentVersion(v.getVersion(), Boolean.TRUE.equals(v.getDefault()))) + .toList(); + return new ComponentVersions(NEXTFLOW, versions); + } +} diff --git a/src/main/java/io/seqera/tower/cli/commands/pipelines/NullableLaunchOptions.java b/src/main/java/io/seqera/tower/cli/commands/pipelines/NullableLaunchOptions.java index 8d114c9d..dc2c78c2 100644 --- a/src/main/java/io/seqera/tower/cli/commands/pipelines/NullableLaunchOptions.java +++ b/src/main/java/io/seqera/tower/cli/commands/pipelines/NullableLaunchOptions.java @@ -40,7 +40,7 @@ public class NullableLaunchOptions { @Option(names = {"--syntax-parser"}, description = "Nextflow language syntax parser version: 'v1' (legacy) or 'v2'. Takes precedence over the value stored in the launch configuration.") public SyntaxParserEnum syntaxParser; - @Option(names = {"--nextflow-version"}, description = "Nextflow version to run the workflow with. Must exist in the Platform version catalog and meet the minimum required by the compute environment. Takes precedence over the value stored in the launch configuration.") + @Option(names = {"--nextflow-version"}, description = "Nextflow version to run the workflow with. Must exist in the Platform version catalog and meet the minimum required by the compute environment (see 'tw info versions'). Takes precedence over the value stored in the launch configuration.") public String nextflowVersion; @Option(names = {"--output-dir"}, description = "Per-run output directory, passed to Nextflow as '-output-dir'. Requires Nextflow 24.10.0 or later and the workflow outputs syntax. Takes precedence over the value stored in the launch configuration.") diff --git a/src/main/java/io/seqera/tower/cli/responses/ComponentVersions.java b/src/main/java/io/seqera/tower/cli/responses/ComponentVersions.java new file mode 100644 index 00000000..4c27c8af --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/responses/ComponentVersions.java @@ -0,0 +1,52 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.responses; + +import io.seqera.tower.cli.utils.TableList; + +import java.io.PrintWriter; +import java.util.List; + +public class ComponentVersions extends Response { + + public record ComponentVersion(String version, boolean isDefault) { + } + + public final String component; + public final List versions; + + public ComponentVersions(String component, List versions) { + this.component = component; + this.versions = versions; + } + + @Override + public void toString(PrintWriter out) { + out.println(ansi(String.format("%n @|bold Available %s versions:|@%n", component))); + if (versions.isEmpty()) { + out.println(ansi(String.format(" @|yellow No %s versions found|@", component))); + return; + } + + TableList table = new TableList(out, 2, "Version", "Default"); + table.setPrefix(" "); + versions.forEach(v -> table.addRow(v.version(), v.isDefault() ? "yes" : "no")); + table.print(); + out.println(""); + } +} diff --git a/src/test/java/io/seqera/tower/cli/InfoCmdTest.java b/src/test/java/io/seqera/tower/cli/InfoCmdTest.java index 40f02332..39de6d49 100644 --- a/src/test/java/io/seqera/tower/cli/InfoCmdTest.java +++ b/src/test/java/io/seqera/tower/cli/InfoCmdTest.java @@ -17,6 +17,8 @@ package io.seqera.tower.cli; import io.seqera.tower.cli.commands.enums.OutputType; +import io.seqera.tower.cli.responses.ComponentVersions; +import io.seqera.tower.cli.responses.ComponentVersions.ComponentVersion; import io.seqera.tower.cli.responses.InfoResponse; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -26,11 +28,13 @@ import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Properties; import static org.apache.commons.lang3.StringUtils.chop; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockserver.matchers.Times.exactly; import static org.mockserver.model.HttpRequest.request; import static org.mockserver.model.HttpResponse.response; @@ -156,6 +160,31 @@ void testInfoStatusUrlFail(MockServerClient mock) throws IOException { assertEquals(chop(new InfoResponse(0,-1,-1, opts).toString()), out.stdOut); } + @Test + void testVersions(MockServerClient mock) { + mock.reset(); + mock.when( + request().withMethod("GET").withPath("/platform/versions"), exactly(1) + ).respond( + response().withStatusCode(200).withBody(""" + { + "nextflowVersions": [ + {"version": "25.10.1", "image": "nf-launcher:j21-25.10.1"}, + {"version": "26.04.6", "image": "nf-launcher:j21-26.04.6", "default": true} + ] + }""").withContentType(MediaType.APPLICATION_JSON) + ); + + ExecOut out = exec(mock, "info", "versions"); + + assertEquals("", out.stdErr); + assertEquals(0, out.exitCode); + assertEquals(chop(new ComponentVersions("nextflow", List.of( + new ComponentVersion("25.10.1", false), + new ComponentVersion("26.04.6", true))).toString()), out.stdOut); + assertTrue(out.stdOut.contains("26.04.6"), out.stdOut); + } + private String getCliVersion() throws IOException { Properties props = new Properties(); props.load(this.getClass().getResourceAsStream("/META-INF/build-info.properties")); From c1c574db5a73fc48ac3a209fb489218b8d8e2862 Mon Sep 17 00:00:00 2001 From: Cristian Ramon-Cortes Date: Fri, 2 Oct 2026 18:42:34 +0200 Subject: [PATCH 3/5] feat(studios): add star and unstar Platform API 1.190.0 added studio starring (POST/DELETE /studios/{sessionId}/star) and the starred flag on studio describe, but tw never wired them up. This adds 'tw studios star|unstar' so CLI users can do what the UI already does. 'studios view' now shows the starred flag so the result of star/unstar is visible. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../seqera/tower/cli/commands/StudiosCmd.java | 4 ++ .../tower/cli/commands/studios/StarCmd.java | 46 ++++++++++++++++ .../tower/cli/commands/studios/UnstarCmd.java | 46 ++++++++++++++++ .../cli/responses/studios/StudioStarred.java | 40 ++++++++++++++ .../cli/responses/studios/StudiosView.java | 1 + .../tower/cli/studios/StudiosCmdTest.java | 53 +++++++++++++++++++ 6 files changed, 190 insertions(+) create mode 100644 src/main/java/io/seqera/tower/cli/commands/studios/StarCmd.java create mode 100644 src/main/java/io/seqera/tower/cli/commands/studios/UnstarCmd.java create mode 100644 src/main/java/io/seqera/tower/cli/responses/studios/StudioStarred.java diff --git a/src/main/java/io/seqera/tower/cli/commands/StudiosCmd.java b/src/main/java/io/seqera/tower/cli/commands/StudiosCmd.java index 345d7d48..8cbb28b0 100644 --- a/src/main/java/io/seqera/tower/cli/commands/StudiosCmd.java +++ b/src/main/java/io/seqera/tower/cli/commands/StudiosCmd.java @@ -21,6 +21,8 @@ import io.seqera.tower.cli.commands.studios.DeleteCmd; import io.seqera.tower.cli.commands.studios.ExtendCmd; import io.seqera.tower.cli.commands.studios.RenameCheckpointCmd; +import io.seqera.tower.cli.commands.studios.StarCmd; +import io.seqera.tower.cli.commands.studios.UnstarCmd; import io.seqera.tower.cli.commands.studios.ListCmd; import io.seqera.tower.cli.commands.studios.AddAsNewCmd; import io.seqera.tower.cli.commands.studios.StartCmd; @@ -45,6 +47,8 @@ StopCmd.class, UpdateCmd.class, DeleteCmd.class, + StarCmd.class, + UnstarCmd.class, ExtendCmd.class, } ) diff --git a/src/main/java/io/seqera/tower/cli/commands/studios/StarCmd.java b/src/main/java/io/seqera/tower/cli/commands/studios/StarCmd.java new file mode 100644 index 00000000..079871b5 --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/commands/studios/StarCmd.java @@ -0,0 +1,46 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.commands.studios; + +import io.seqera.tower.ApiException; +import io.seqera.tower.cli.commands.global.WorkspaceOptionalOptions; +import io.seqera.tower.cli.responses.Response; +import io.seqera.tower.cli.responses.studios.StudioStarred; +import picocli.CommandLine; + +@CommandLine.Command( + name = "star", + description = "Add a studio to your starred studios." +) +public class StarCmd extends AbstractStudiosCmd { + + @CommandLine.Mixin + public WorkspaceOptionalOptions workspace; + + @CommandLine.Mixin + public StudioRefOptions studioRefOptions; + + @Override + protected Response exec() throws ApiException { + Long wspId = workspaceId(workspace.workspace); + String sessionId = getSessionId(studioRefOptions, wspId); + + studiosApi().createDataStudioStar(sessionId, wspId); + + return new StudioStarred(studioRefOptions.getStudioIdentifier(), workspaceRef(wspId), true); + } +} diff --git a/src/main/java/io/seqera/tower/cli/commands/studios/UnstarCmd.java b/src/main/java/io/seqera/tower/cli/commands/studios/UnstarCmd.java new file mode 100644 index 00000000..b47d2dc0 --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/commands/studios/UnstarCmd.java @@ -0,0 +1,46 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.commands.studios; + +import io.seqera.tower.ApiException; +import io.seqera.tower.cli.commands.global.WorkspaceOptionalOptions; +import io.seqera.tower.cli.responses.Response; +import io.seqera.tower.cli.responses.studios.StudioStarred; +import picocli.CommandLine; + +@CommandLine.Command( + name = "unstar", + description = "Remove a studio from your starred studios." +) +public class UnstarCmd extends AbstractStudiosCmd { + + @CommandLine.Mixin + public WorkspaceOptionalOptions workspace; + + @CommandLine.Mixin + public StudioRefOptions studioRefOptions; + + @Override + protected Response exec() throws ApiException { + Long wspId = workspaceId(workspace.workspace); + String sessionId = getSessionId(studioRefOptions, wspId); + + studiosApi().deleteDataStudioStar(sessionId, wspId); + + return new StudioStarred(studioRefOptions.getStudioIdentifier(), workspaceRef(wspId), false); + } +} diff --git a/src/main/java/io/seqera/tower/cli/responses/studios/StudioStarred.java b/src/main/java/io/seqera/tower/cli/responses/studios/StudioStarred.java new file mode 100644 index 00000000..2a8ed4e2 --- /dev/null +++ b/src/main/java/io/seqera/tower/cli/responses/studios/StudioStarred.java @@ -0,0 +1,40 @@ +/* + * Copyright 2021-2026, Seqera. + * + * 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 io.seqera.tower.cli.responses.studios; + +import io.seqera.tower.cli.responses.Response; + +public class StudioStarred extends Response { + + public final String userSuppliedStudioIdentifier; + public final String workspaceRef; + public final boolean starred; + + public StudioStarred(String userSuppliedStudioIdentifier, String workspaceRef, boolean starred) { + this.userSuppliedStudioIdentifier = userSuppliedStudioIdentifier; + this.workspaceRef = workspaceRef; + this.starred = starred; + } + + @Override + public String toString() { + if (starred) { + return ansi(String.format("%n @|yellow Studio %s starred at %s workspace.|@%n", userSuppliedStudioIdentifier, workspaceRef)); + } + return ansi(String.format("%n @|yellow Studio %s unstarred at %s workspace.|@%n", userSuppliedStudioIdentifier, workspaceRef)); + } +} diff --git a/src/main/java/io/seqera/tower/cli/responses/studios/StudiosView.java b/src/main/java/io/seqera/tower/cli/responses/studios/StudiosView.java index a74ff62f..3af4b146 100644 --- a/src/main/java/io/seqera/tower/cli/responses/studios/StudiosView.java +++ b/src/main/java/io/seqera/tower/cli/responses/studios/StudiosView.java @@ -74,6 +74,7 @@ public void toString(PrintWriter out){ table.addRow("Memory allocated", config == null ? "-" : String.valueOf(config.getMemory())); table.addRow("Build reports", studio.getWaveBuildUrl() == null ? "NA" : studio.getWaveBuildUrl()); table.addRow("Private", studio.getIsPrivate() == null ? "NA" : String.valueOf(studio.getIsPrivate())); + table.addRow("Starred", studio.getStarred() == null ? "NA" : String.valueOf(studio.getStarred())); table.addRow("Lifespan (hours)", studio.getEffectiveLifespanHours() == null ? "Unlimited" : String.valueOf(studio.getEffectiveLifespanHours())); table.addRow("SSH enabled", config == null || config.getSshEnabled() == null ? "false" : String.valueOf(config.getSshEnabled())); diff --git a/src/test/java/io/seqera/tower/cli/studios/StudiosCmdTest.java b/src/test/java/io/seqera/tower/cli/studios/StudiosCmdTest.java index f2cd2722..f46b3556 100644 --- a/src/test/java/io/seqera/tower/cli/studios/StudiosCmdTest.java +++ b/src/test/java/io/seqera/tower/cli/studios/StudiosCmdTest.java @@ -32,6 +32,7 @@ import io.seqera.tower.cli.responses.studios.StudioDeleted; import io.seqera.tower.cli.responses.studios.StudioCheckpointUpdated; import io.seqera.tower.cli.responses.studios.StudioLifespanExtended; +import io.seqera.tower.cli.responses.studios.StudioStarred; import io.seqera.tower.cli.responses.studios.StudioStartSubmitted; import io.seqera.tower.cli.responses.studios.StudioStopSubmitted; import io.seqera.tower.cli.responses.studios.StudioUpdated; @@ -2867,6 +2868,58 @@ private void mockWorkspace(MockServerClient mock) { ); } + @ParameterizedTest + @EnumSource(OutputType.class) + void testStar(OutputType format, MockServerClient mock) { + mockWorkspace(mock); + mock.when( + request().withMethod("POST").withPath("/studios/3e8370e7/star").withQueryStringParameter("workspaceId", "75887156211589"), exactly(1) + ).respond( + response().withStatusCode(200).withBody("{\"sessionId\": \"3e8370e7\"}").withContentType(MediaType.APPLICATION_JSON) + ); + + ExecOut out = exec(format, mock, "studios", "star", "-w", "75887156211589", "-i", "3e8370e7"); + + assertOutput(format, out, new StudioStarred("3e8370e7", "[organization1 / workspace1]", true)); + mock.verify(request().withMethod("POST").withPath("/studios/3e8370e7/star"), VerificationTimes.once()); + } + + @ParameterizedTest + @EnumSource(OutputType.class) + void testUnstarByName(OutputType format, MockServerClient mock) { + mockWorkspace(mock); + mock.when( + request().withMethod("GET").withPath("/studios").withQueryStringParameter("workspaceId", "75887156211589"), exactly(1) + ).respond( + response().withStatusCode(200).withBody(loadResource("studios/studios_list_response")).withContentType(MediaType.APPLICATION_JSON) + ); + mock.when( + request().withMethod("DELETE").withPath("/studios/3e8370e7/star").withQueryStringParameter("workspaceId", "75887156211589"), exactly(1) + ).respond( + response().withStatusCode(204) + ); + + ExecOut out = exec(format, mock, "studios", "unstar", "-w", "75887156211589", "-n", "studio-a66d"); + + assertOutput(format, out, new StudioStarred("studio-a66d", "[organization1 / workspace1]", false)); + mock.verify(request().withMethod("DELETE").withPath("/studios/3e8370e7/star"), VerificationTimes.once()); + } + + @Test + void testUnstarNotStarred(MockServerClient mock) { + mockWorkspace(mock); + mock.when( + request().withMethod("DELETE").withPath("/studios/3e8370e7/star").withQueryStringParameter("workspaceId", "75887156211589"), exactly(1) + ).respond( + response().withStatusCode(404).withBody("{\"message\": \"Studio '3e8370e7' is not starred\"}").withContentType(MediaType.APPLICATION_JSON) + ); + + ExecOut out = exec(mock, "studios", "unstar", "-w", "75887156211589", "-i", "3e8370e7"); + + assertEquals(1, out.exitCode); + assertTrue(out.stdErr.contains("Studio '3e8370e7' is not starred"), out.stdErr); + } + @ParameterizedTest @EnumSource(OutputType.class) void testExtend(OutputType format, MockServerClient mock) { From de21fd7a4b0678df9da421677141574cbd9364b7 Mon Sep 17 00:00:00 2001 From: Cristian Ramon-Cortes Date: Fri, 2 Oct 2026 18:42:57 +0200 Subject: [PATCH 4/5] docs: document the commands added for API 1.190 An Agents section, plus 'Other additions' entries for 'tw info versions' and 'tw studios star|unstar'. Examples use bash code blocks, as markdownlint MD014 rejects $-prompted blocks that show no output. Co-Authored-By: Claude Opus 5.5 (1M context) --- USAGE.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/USAGE.md b/USAGE.md index 31f38bb1..1685ee02 100644 --- a/USAGE.md +++ b/USAGE.md @@ -756,6 +756,14 @@ tw managed-identities add -o my-org -n hpc1 -p slurm -H login.hpc.example.com tw managed-identities credentials -o my-org -n hpc1 add -l my_linux_user -k ~/.ssh/id_rsa ``` +## Agents + +Run `tw agents -h` to view the supported operations: `list`, `view`, `add`, `update`, `delete`, `enable`, and `disable`. Agents must be enabled for the organization. + +```bash +tw agents add -w my-org/my-workspace -n triage --instructions-file triage.md +``` + ## Audit logs Run `tw audit-logs -h` to view the supported operations: `list`, `view`, and `export`. These commands require a root user. @@ -773,11 +781,11 @@ tw audit-logs export --after 2026-09-01T00:00:00Z --before 2026-09-30T23:59:59Z - AWS Cloud: `--log-group`. Google Cloud: `--project-id`. - HPC platforms: `--propagate-head-job-options`. - Seqera Compute: `--data-retention-policy`. -- `tw launch`, `tw pipelines add|update`, `tw runs relaunch`, and `tw actions add|update`: `--tower-config`. +- `tw launch`, `tw pipelines add|update`, `tw runs relaunch`, and `tw actions add|update`: `--tower-config`. List the Nextflow versions you can launch with `tw info versions`. - `tw runs star|unstar`, `tw runs view log [--next ]`, and `tw runs delete -i ,`. - `tw pipelines schema`: show a pipeline's parameter schema. - `tw actions add cron|bucket`. -- `tw studios extend|rename-checkpoint`. `--mount-data-uris` also accepts a folder inside a data link. +- `tw studios star|unstar|extend|rename-checkpoint`. `--mount-data-uris` also accepts a folder inside a data link. - `tw data-links delete-content|download-script`. - `tw datasets add --url`, `tw datasets update --url`, and `tw datasets disable-version`. From e27046bd6384d8fe63151211da75f027c6e1ccad Mon Sep 17 00:00:00 2001 From: Cristian Ramon-Cortes Date: Fri, 2 Oct 2026 18:43:14 +0200 Subject: [PATCH 5/5] build(native): register reflection metadata for the API 1.190 commands The agents, 'info versions' and 'studios star|unstar' commands and their responses are new tw classes, so the native image needs their reflection entries for picocli and Jackson. Entries copied from the full catch-up branch, where a test run under the GraalVM tracing agent captured them. Co-Authored-By: Claude Opus 5.5 (1M context) --- conf/reflect-config.json | 151 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/conf/reflect-config.json b/conf/reflect-config.json index 0bc736cb..a253af03 100644 --- a/conf/reflect-config.json +++ b/conf/reflect-config.json @@ -166,6 +166,12 @@ "allDeclaredMethods":true, "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"io.seqera.tower.cli.commands.AgentsCmd", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }] +}, { "name":"io.seqera.tower.cli.commands.AuditLogsCmd", "queryAllDeclaredMethods":true, @@ -409,6 +415,81 @@ "allDeclaredMethods":true, "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"io.seqera.tower.cli.commands.agents.AbstractAgentsCmd", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"io.seqera.tower.cli.commands.agents.AddCmd", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"description"}, {"name":"githubAppCredentialsId"}, {"name":"instructions"}, {"name":"name"}, {"name":"serviceAccountId"}, {"name":"workspace"}], + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"io.seqera.tower.cli.commands.agents.AgentInstructionsOptions", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"file"}, {"name":"text"}], + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"io.seqera.tower.cli.commands.agents.AgentRefOptions", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"agent"}], + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"io.seqera.tower.cli.commands.agents.AgentRefOptions$AgentRef", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"id"}, {"name":"name"}], + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"io.seqera.tower.cli.commands.agents.DeleteCmd", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"ref"}, {"name":"workspace"}], + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"io.seqera.tower.cli.commands.agents.DisableCmd", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"ref"}, {"name":"workspace"}], + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"io.seqera.tower.cli.commands.agents.EnableCmd", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"ref"}, {"name":"workspace"}], + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"io.seqera.tower.cli.commands.agents.ListCmd", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"filter"}, {"name":"paginationOptions"}, {"name":"workspace"}], + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"io.seqera.tower.cli.commands.agents.UpdateCmd", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"description"}, {"name":"githubAppCredentialsId"}, {"name":"instructions"}, {"name":"newName"}, {"name":"ref"}, {"name":"serviceAccountId"}, {"name":"workspace"}], + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"io.seqera.tower.cli.commands.agents.ViewCmd", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"ref"}, {"name":"workspace"}], + "methods":[{"name":"","parameterTypes":[] }] +}, { "name":"io.seqera.tower.cli.commands.auditlogs.AuditLogsFilterOptions", "queryAllDeclaredMethods":true, @@ -1485,6 +1566,13 @@ "allDeclaredMethods":true, "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"io.seqera.tower.cli.commands.info.VersionsCmd", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"component"}, {"name":"fusion"}, {"name":"nextflow"}], + "methods":[{"name":"","parameterTypes":[] }] +}, { "name":"io.seqera.tower.cli.commands.labels.AbstractLabelsCmd", "allDeclaredFields":true, @@ -2305,6 +2393,13 @@ "fields":[{"name":"checkpointId"}, {"name":"newName"}, {"name":"studioRefOptions"}, {"name":"workspace"}], "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"io.seqera.tower.cli.commands.studios.StarCmd", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"studioRefOptions"}, {"name":"workspace"}], + "methods":[{"name":"","parameterTypes":[] }] +}, { "name":"io.seqera.tower.cli.commands.studios.StartCmd", "allDeclaredFields":true, @@ -2372,6 +2467,13 @@ "queryAllDeclaredMethods":true, "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"io.seqera.tower.cli.commands.studios.UnstarCmd", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "fields":[{"name":"studioRefOptions"}, {"name":"workspace"}], + "methods":[{"name":"","parameterTypes":[] }] +}, { "name":"io.seqera.tower.cli.commands.studios.UpdateCmd", "allDeclaredFields":true, @@ -2555,6 +2657,19 @@ "allDeclaredConstructors":true, "methods":[{"name":"","parameterTypes":[] }, {"name":"setMessage","parameterTypes":["java.lang.String"] }] }, +{ + "name":"io.seqera.tower.cli.responses.ComponentVersions", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"io.seqera.tower.cli.responses.ComponentVersions$ComponentVersion", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"isDefault","parameterTypes":[] }, {"name":"version","parameterTypes":[] }] +}, { "name":"io.seqera.tower.cli.responses.CredentialsAdded", "allDeclaredFields":true, @@ -2646,6 +2761,36 @@ "allDeclaredMethods":true, "allDeclaredConstructors":true }, +{ + "name":"io.seqera.tower.cli.responses.agents.AgentAdded", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"io.seqera.tower.cli.responses.agents.AgentDeleted", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"io.seqera.tower.cli.responses.agents.AgentUpdated", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"io.seqera.tower.cli.responses.agents.AgentView", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"io.seqera.tower.cli.responses.agents.AgentsList", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, { "name":"io.seqera.tower.cli.responses.auditlogs.AuditLogView", "allDeclaredFields":true, @@ -3363,6 +3508,12 @@ "queryAllDeclaredMethods":true, "queryAllDeclaredConstructors":true }, +{ + "name":"io.seqera.tower.cli.responses.studios.StudioStarred", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, { "name":"io.seqera.tower.cli.responses.studios.StudioStartSubmitted", "allDeclaredFields":true,