Skip to content
Open
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
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.version>3.30.6</quarkus.platform.version>
<quarkus.platform.version>3.37.4</quarkus.platform.version>
<surefire-plugin.version>3.2.5</surefire-plugin.version>
<compiler-plugin.version>3.13.0</compiler-plugin.version>
<checkstyle-plugin.version>3.6.0</checkstyle-plugin.version>
Expand Down Expand Up @@ -482,4 +482,4 @@
</build>
</profile>
</profiles>
</project>
</project>
5 changes: 4 additions & 1 deletion src/main/java/com/streamx/cli/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ public int run(String... args) throws Exception {
Object lastCommand = parsed.get(parsed.size() - 1).getCommand();
if (lastCommand instanceof AbstractCommand<?> abstractCommand) {
try {
abstractCommand.populateStreamxHome();
abstractCommand.populateStreamxHome(parsed);
// -H/--context are applied now; refresh the root help header to reflect them.
SynopsisHelper.applyRootUsageLayout(parsed.get(0));
} catch (Exception e) {
return abstractCommand.handleExecutionError(e);
}
Expand All @@ -36,6 +38,7 @@ public int run(String... args) throws Exception {
});

SynopsisHelper.applyCustomSynopses(commandLine);
SynopsisHelper.applyRootUsageLayout(commandLine);

return commandLine.execute(args);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.streamx.cli.commands;

import com.streamx.cli.commands.completion.CompleteContextNamesCommand;
import com.streamx.cli.commands.completion.CompleteNonDefaultTemplateIdsCommand;
import com.streamx.cli.commands.completion.CompleteRegisteredTemplateIdsCommand;
import com.streamx.cli.commands.completion.CompleteSettingsKeysCommand;
import com.streamx.cli.commands.completion.CompleteSettingsSetKeysCommand;
import com.streamx.cli.commands.completion.CompleteTemplateIdsCommand;
import com.streamx.cli.commands.completion.CompletionCommand;
import com.streamx.cli.commands.context.ContextCommand;
import com.streamx.cli.commands.local.LocalCommand;
import com.streamx.cli.commands.publish.PublishCommand;
import com.streamx.cli.commands.settings.SettingsCommand;
Expand All @@ -16,6 +18,7 @@
name = "streamx",
header = "StreamX CLI. More info at https://streamx.com",
subcommands = {
ContextCommand.class,
LocalCommand.class,
SettingsCommand.class,
PublishCommand.class,
Expand All @@ -24,7 +27,8 @@
CompleteRegisteredTemplateIdsCommand.class,
CompleteNonDefaultTemplateIdsCommand.class,
CompleteSettingsKeysCommand.class,
CompleteSettingsSetKeysCommand.class
CompleteSettingsSetKeysCommand.class,
CompleteContextNamesCommand.class
}
)
public class StreamxCommand extends AbstractCommandGroup {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.streamx.cli.commands.completion;

import com.streamx.cli.config.StreamxHome;
import com.streamx.cli.framework.AbstractCommand;
import com.streamx.cli.framework.CommandResult;
import java.util.List;
import picocli.CommandLine;

@CommandLine.Command(
name = "__complete-context-names",
hidden = true,
header = "Internal: list every context name, one per line"
)
public class CompleteContextNamesCommand extends AbstractCommand<List<String>> {

@Override
public boolean needsContext() {
return false;
}

@Override
public CommandResult<List<String>> runCommand() {
return new CommandResult<>(StreamxHome.listContextNames());
}

@Override
public String getTextOutput(CommandResult<List<String>> result) {
return String.join("\n", result.getData());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.streamx.cli.commands.settings.eventtemplates.NonDefaultTemplateIdCompletionCandidates;
import com.streamx.cli.commands.settings.eventtemplates.RegisteredTemplateIdCompletionCandidates;
import com.streamx.cli.commands.settings.eventtemplates.TemplateIdCompletionCandidates;
import com.streamx.cli.config.ContextNameCompletionCandidates;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
Expand All @@ -17,6 +18,7 @@

public final class ZshCompletionGenerator {


private ZshCompletionGenerator() {
}

Expand Down Expand Up @@ -169,7 +171,7 @@ private static String getOptionArgSpec(OptionSpec opt) {
return "";
}
String label = opt.paramLabel();
String action = getCompletionAction(opt.type(), opt, null);
String action = getCompletionAction(opt.type(), opt, opt.completionCandidates());
if (label == null || label.isEmpty()) {
label = "value";
}
Expand Down Expand Up @@ -210,6 +212,16 @@ private static String getCompletionAction(
if (completionCandidates instanceof SettingsKeyCompletionCandidates) {
return "($(streamx __complete-settings-keys 2>/dev/null))";
}
if (completionCandidates instanceof ContextNameCompletionCandidates) {
return "($(streamx __complete-context-names 2>/dev/null))";
}
// Any remaining candidates are a fixed list (e.g. roles); the dynamic ones are handled above.
if (completionCandidates != null) {
String values = renderCandidates(completionCandidates);
if (!values.isEmpty()) {
return values;
}
}
if (type != null && type.isEnum()) {
Object[] constants = type.getEnumConstants();
StringBuilder values = new StringBuilder("(");
Expand All @@ -228,6 +240,19 @@ private static String getCompletionAction(
return "";
}

private static String renderCandidates(Iterable<String> completionCandidates) {
StringBuilder values = new StringBuilder("(");
boolean empty = true;
for (String candidate : completionCandidates) {
if (!empty) {
values.append(" ");
}
values.append(escape(candidate));
empty = false;
}
return empty ? "" : values.append(")").toString();
}

private static String preferredOptionName(OptionSpec opt) {
String shortName = null;
String longName = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.streamx.cli.commands.context;

import com.streamx.cli.commands.context.create.CreateCommand;
import com.streamx.cli.commands.context.current.CurrentCommand;
import com.streamx.cli.commands.context.delete.DeleteCommand;
import com.streamx.cli.commands.context.list.ListCommand;
import com.streamx.cli.commands.context.use.UseCommand;
import com.streamx.cli.framework.AbstractCommandGroup;
import picocli.CommandLine;

@CommandLine.Command(
name = "context",
header = "Manage StreamX contexts (bundled settings, event templates and login "
+ "per environment)",
subcommands = {
ListCommand.class,
CreateCommand.class,
UseCommand.class,
CurrentCommand.class,
DeleteCommand.class
}
)
public class ContextCommand extends AbstractCommandGroup {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.streamx.cli.commands.context.create;

import static com.streamx.cli.i18n.MessageProvider.msg;

import com.streamx.cli.config.ContextNameCompletionCandidates;
import com.streamx.cli.config.StreamxHome;
import com.streamx.cli.framework.AbstractSilentCommand;
import com.streamx.cli.framework.CliException;
import com.streamx.cli.framework.CommandResult;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.stream.Stream;
import picocli.CommandLine;

@CommandLine.Command(
name = "create",
header = "Create a context and switch to it",
description = "The new context starts with empty settings and becomes the current context."
)
public class CreateCommand extends AbstractSilentCommand {

@CommandLine.Parameters(index = "0", description = "Context name (lowercase, digits, dashes)")
public String name;

@CommandLine.Option(
names = "--from",
description = "Copy settings and event templates (not the login) from this context",
completionCandidates = ContextNameCompletionCandidates.class
)
public String from;

@Override
public boolean needsContext() {
return false;
}

@Override
public CommandResult<Void> runCommand() {
StreamxHome.requireValidContextName(name);
if (StreamxHome.contextExists(name)) {
throw new CliException(msg.contextAlreadyExists(name));
}
if (from != null) {
StreamxHome.requireValidContextName(from);
if (!StreamxHome.contextExists(from)) {
throw new CliException(msg.contextNotFound(from));
}
}

try {
Files.createDirectories(StreamxHome.getConfigDirOf(name));
Files.createDirectories(StreamxHome.getEventTemplatesDirOf(name));
if (from != null) {
Path source = StreamxHome.getConfigDirOf(from).resolve("application.properties");
if (Files.isRegularFile(source)) {
Files.copy(source, StreamxHome.getConfigDirOf(name).resolve("application.properties"));
}
copyTree(
StreamxHome.getEventTemplatesDirOf(from),
StreamxHome.getEventTemplatesDirOf(name));
}
} catch (IOException e) {
throw new CliException(msg.contextCreateFailed(name, e.getMessage()), e);
}

System.out.println(msg.contextCreated(name));
try {
StreamxHome.writeCurrentContextPointer(name);
System.out.println(msg.contextSwitched(name));
} catch (IOException e) {
throw new CliException(msg.contextSwitchFailed(e.getMessage()), e);
}
System.err.println(msg.contextCreateConfigureHint());
return new CommandResult<>(null);
}

private static void copyTree(Path source, Path target) throws IOException {
if (!Files.isDirectory(source)) {
return;
}
try (Stream<Path> paths = Files.walk(source)) {
for (Path path : paths.toList()) {
Path destination = target.resolve(source.relativize(path).toString());
if (Files.isDirectory(path)) {
Files.createDirectories(destination);
} else {
Files.copy(path, destination, StandardCopyOption.REPLACE_EXISTING);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.streamx.cli.commands.context.current;

import com.streamx.cli.config.StreamxHome;
import com.streamx.cli.framework.AbstractSilentCommand;
import com.streamx.cli.framework.CommandResult;
import picocli.CommandLine;

@CommandLine.Command(
name = "current",
header = "Print the active context name"
)
public class CurrentCommand extends AbstractSilentCommand {

@Override
public boolean needsContext() {
return false;
}

@Override
public CommandResult<Void> runCommand() {
System.out.println(StreamxHome.getActiveContext());
return new CommandResult<>(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.streamx.cli.commands.context.delete;

import static com.streamx.cli.i18n.MessageProvider.msg;

import com.streamx.cli.config.ContextNameCompletionCandidates;
import com.streamx.cli.config.StreamxHome;
import com.streamx.cli.framework.AbstractSilentCommand;
import com.streamx.cli.framework.CliException;
import com.streamx.cli.framework.CommandResult;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.stream.Stream;
import picocli.CommandLine;

@CommandLine.Command(
name = "delete",
header = "Delete a context",
description = "Removes the context's settings, event templates and stored login from this "
+ "machine. The login is not revoked; run 'streamx auth logout' in the context first."
)
public class DeleteCommand extends AbstractSilentCommand {

@CommandLine.Parameters(
index = "0",
description = "Context name",
completionCandidates = ContextNameCompletionCandidates.class
)
public String name;

@Override
public boolean needsContext() {
return false;
}

@Override
public CommandResult<Void> runCommand() {
StreamxHome.requireValidContextName(name);
if (!StreamxHome.contextExists(name)) {
throw new CliException(msg.contextDoesNotExist(name));
}
if (name.equals(StreamxHome.getActiveContext())) {
throw new CliException(msg.contextCannotDeleteActive(name));
}
if (name.equals(StreamxHome.readCurrentContextPointer())) {
throw new CliException(msg.contextCannotDeleteCurrent(name));
}

Path contextDir = StreamxHome.getContextDirOf(name);
boolean hadLogin =
Files.isRegularFile(StreamxHome.getConfigDirOf(name).resolve("credentials.json"));
try (Stream<Path> paths = Files.walk(contextDir)) {
paths.sorted(Comparator.reverseOrder()).forEach(path -> {
try {
Files.delete(path);
} catch (IOException e) {
throw new CliException(msg.contextDeleteFailed(name, e.getMessage()), e);
}
});
} catch (IOException e) {
throw new CliException(msg.contextDeleteFailed(name, e.getMessage()), e);
}

System.out.println(msg.contextDeleted(name));
if (hadLogin) {
System.err.println(msg.contextDeletedLoginNote());
}
return new CommandResult<>(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.streamx.cli.commands.context.list;

import io.quarkus.runtime.annotations.RegisterForReflection;

@RegisterForReflection
public record ContextInfo(String name, boolean active, String platformUrl, boolean loggedIn) {
}
Loading
Loading