From 8a86dfc138d6e8173965f78cbeda311ee3bbe09c Mon Sep 17 00:00:00 2001 From: Svjatoslav Agejenko Date: Thu, 10 Apr 2025 23:35:23 +0300 Subject: [PATCH] Wizard usability improvements --- .../eu/svjatoslav/alyverkko_cli/Command.java | 6 +- .../eu/svjatoslav/alyverkko_cli/Main.java | 6 +- .../commands/JoinFilesCommand.java | 4 +- .../commands/ListModelsCommand.java | 4 +- .../alyverkko_cli/commands/WizardCommand.java | 168 ++++++++++-------- .../MailCorrespondentCommand.java | 4 +- 6 files changed, 101 insertions(+), 91 deletions(-) diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/Command.java b/src/main/java/eu/svjatoslav/alyverkko_cli/Command.java index 30748ca..dfd9b14 100644 --- a/src/main/java/eu/svjatoslav/alyverkko_cli/Command.java +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/Command.java @@ -12,15 +12,15 @@ public interface Command { /** * @return the subcommand's name. */ - String getName(); + String getCommandName(); /** - * Called to carry out the specific subcommand. Typically reads + * Called to carry out the specific subcommand. Typically, reads * command-line arguments and performs the desired action. * * @param args arguments passed after the subcommand name. * @throws IOException if I/O operations fail. * @throws InterruptedException if the operation is interrupted. */ - void execute(String[] args) throws IOException, InterruptedException; + void executeCommand(String[] args) throws IOException, InterruptedException; } diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/Main.java b/src/main/java/eu/svjatoslav/alyverkko_cli/Main.java index ed81cfb..48bba6c 100644 --- a/src/main/java/eu/svjatoslav/alyverkko_cli/Main.java +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/Main.java @@ -58,7 +58,7 @@ public class Main { String commandName = args[0].toLowerCase(); Optional commandOptional = commands.stream() - .filter(cmd -> cmd.getName().equals(commandName)) + .filter(cmd -> cmd.getCommandName().equals(commandName)) .findFirst(); if (!commandOptional.isPresent()) { @@ -69,7 +69,7 @@ public class Main { Command command = commandOptional.get(); String[] remainingArgs = copyOfRange(args, 1, args.length); - command.execute(remainingArgs); + command.executeCommand(remainingArgs); } /** @@ -78,6 +78,6 @@ public class Main { private void showHelp() { System.out.println("Älyverkko CLI\n"); System.out.println("Available commands:"); - commands.forEach(cmd -> System.out.println(" " + cmd.getName())); + commands.forEach(cmd -> System.out.println(" " + cmd.getCommandName())); } } diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/JoinFilesCommand.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/JoinFilesCommand.java index 1e644ed..072bb3a 100644 --- a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/JoinFilesCommand.java +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/JoinFilesCommand.java @@ -81,7 +81,7 @@ public class JoinFilesCommand implements Command { * @return the name of this command, i.e., "joinfiles". */ @Override - public String getName() { + public String getCommandName() { return "joinfiles"; } @@ -94,7 +94,7 @@ public class JoinFilesCommand implements Command { * @throws IOException if any IO operations fail. */ @Override - public void execute(String[] cliArguments) throws IOException { + public void executeCommand(String[] cliArguments) throws IOException { configuration = loadConfiguration(getConfigurationFile(null)); if (configuration == null){ System.out.println("Failed to load configuration file"); diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/ListModelsCommand.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/ListModelsCommand.java index 56128a6..5078177 100644 --- a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/ListModelsCommand.java +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/ListModelsCommand.java @@ -19,7 +19,7 @@ public class ListModelsCommand implements Command { * @return the name of this command, i.e., "listmodels". */ @Override - public String getName() { + public String getCommandName() { return "listmodels"; } @@ -31,7 +31,7 @@ public class ListModelsCommand implements Command { * @throws IOException if loading configuration fails. */ @Override - public void execute(String[] cliArguments) throws IOException { + public void executeCommand(String[] cliArguments) throws IOException { configuration = loadConfiguration(getConfigurationFile(null)); if (configuration == null){ System.out.println("Failed to load configuration file"); diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/WizardCommand.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/WizardCommand.java index b4832c4..5b1ff9d 100644 --- a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/WizardCommand.java +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/WizardCommand.java @@ -3,13 +3,12 @@ package eu.svjatoslav.alyverkko_cli.commands; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import eu.svjatoslav.alyverkko_cli.Command; -import eu.svjatoslav.alyverkko_cli.Utils; import eu.svjatoslav.alyverkko_cli.configuration.Configuration; +import eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper; import eu.svjatoslav.alyverkko_cli.configuration.ConfigurationModel; import eu.svjatoslav.commons.cli_helper.CLIHelper; import eu.svjatoslav.commons.cli_helper.parameter_parser.Parser; import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.FileOption; -import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.NullOption; import org.apache.commons.lang3.StringUtils; import java.io.*; @@ -19,9 +18,7 @@ import java.util.stream.Collectors; import static eu.svjatoslav.alyverkko_cli.Utils.printRedMessageToConsole; import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.getConfigurationFile; -import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.loadConfiguration; import static eu.svjatoslav.commons.cli_helper.CLIHelper.*; -import static java.lang.Boolean.TRUE; /** * A single WizardCommand that: @@ -38,37 +35,37 @@ import static java.lang.Boolean.TRUE; public class WizardCommand implements Command { // Command-line parser to handle wizard arguments - private final Parser parser = new Parser(); - - // If present, force overwriting existing config - private final NullOption forceOverwriteOption = parser.add(new NullOption("Force overwrite existing configuration")) - .addAliases("--force", "-f"); + private final Parser cliParser = new Parser(); /** * Optional CLI argument for specifying a configuration file path. */ - public FileOption configFileOption = parser.add(new FileOption("Configuration file path")) + public FileOption configFileOption = cliParser.add(new FileOption("Configuration file path")) .addAliases("--config", "-c"); // The config object (loaded or newly created) - private Configuration config; + private Configuration configuration; + + private File configurationFile; @Override - public String getName() { + public String getCommandName() { return "wizard"; } @Override - public void execute(String[] cliArguments) throws IOException { - if (!parser.parse(cliArguments)) { + public void executeCommand(String[] cliArguments) throws IOException { + if (!cliParser.parse(cliArguments)) { System.out.println("Failed to parse command-line arguments"); - parser.showHelp(); + cliParser.showHelp(); return; } + configurationFile = getConfigurationFile(configFileOption); + loadOrCreateConfiguration(); + + // TODO: ------- continue from here -------- - // 1. Load existing config if possible - File configFile = loadOrCreateConfiguration(); // 2. Validate and fix each parameter checkAndFixAllParameters(); @@ -77,7 +74,7 @@ public class WizardCommand implements Command { fixModelEntries(); // 4. If the user is satisfied, attempt to save - trySaveConfiguration(configFile); + trySaveConfiguration(this.configurationFile); // 5. Final selftest pass boolean finalOk = doFinalCheck(); @@ -88,23 +85,44 @@ public class WizardCommand implements Command { } else { System.out.println("\nSome checks did not pass, but we've saved the best config we could."); System.out.println("You might want to run 'wizard' again or manually edit the YAML file:\n " - + configFile.getAbsolutePath() + "\n"); + + this.configurationFile.getAbsolutePath() + "\n"); } } - private File loadOrCreateConfiguration() throws IOException { - File configFile = getConfigurationFile(configFileOption); + private void loadOrCreateConfiguration() throws IOException { + validateConfigurationFile(); - if (configFile.exists() && configFile.isFile()) { - System.out.println("Found existing configuration at " + configFile.getAbsolutePath()); - System.out.println("I will check whether everything is valid, and if not, allow you to fix it.\n"); - config = loadConfiguration(configFile); + if (configurationFile.exists()) { + System.out.println("Found existing configuration at: \"" + configurationFile.getAbsolutePath() + "\""); + configuration = ConfigurationHelper.loadConfiguration(configurationFile); } else { // If no config found, create a fresh one - System.out.println("No existing configuration found. Let's create one!\n"); - config = new Configuration(); + System.out.println("Existing configuration not found at \"" + + configurationFile.getAbsolutePath() + "\". Initializing new blank configuration."); + configuration = new Configuration(); + } + } + + private void validateConfigurationFile() { + if (!configurationFile.exists()) return; // No need to check further if it doesn't exist + + if (configurationFile.isDirectory()) { + System.err.println("ERROR: Configuration file path incorrectly points to a directory: \"" + configurationFile.getAbsolutePath() + + "\". Please specify a file instead."); + System.exit(1); + } + + if (!configurationFile.canRead()) { + System.err.println("ERROR: Cannot read configuration file: \"" + configurationFile.getAbsolutePath() + + "\". Please check permissions."); + System.exit(1); + } + + if (!configurationFile.canWrite()) { + System.err.println("ERROR: Cannot write to configuration file: \"" + configurationFile.getAbsolutePath() + + "\". Please file check permissions."); + System.exit(1); } - return configFile; } /** @@ -115,7 +133,7 @@ public class WizardCommand implements Command { // 2.2 models_directory while (true) { - File modelsDir = config.getModelsDirectory(); + File modelsDir = configuration.getModelsDirectory(); if (modelsDir == null || !modelsDir.isDirectory() || !modelsDir.exists()) { System.out.println("Models directory is missing or invalid."); String defaultVal = (modelsDir == null) ? "~/.config/alyverkko-cli/models" : modelsDir.getPath(); @@ -124,7 +142,7 @@ public class WizardCommand implements Command { defaultVal ); modelsDir = new File(userInput); - config.setModelsDirectory(modelsDir); + configuration.setModelsDirectory(modelsDir); // Attempt to create if not exist if (!modelsDir.exists()) { @@ -137,13 +155,13 @@ public class WizardCommand implements Command { if (askBoolean("Models directory is: " + modelsDir.getAbsolutePath() + " . Is this OK?", true)) { break; } - config.setModelsDirectory(null); + configuration.setModelsDirectory(null); } } // 2.3 prompts_directory while (true) { - File promptsDir = config.getPromptsDirectory(); + File promptsDir = configuration.getPromptsDirectory(); if (promptsDir == null || !promptsDir.isDirectory() || !promptsDir.exists()) { System.out.println("Prompts directory is missing or invalid."); String defaultVal = (promptsDir == null) ? "~/.config/alyverkko-cli/prompts" : promptsDir.getPath(); @@ -152,7 +170,7 @@ public class WizardCommand implements Command { defaultVal ); promptsDir = new File(userInput); - config.setPromptsDirectory(promptsDir); + configuration.setPromptsDirectory(promptsDir); // Attempt to create if not exist if (!promptsDir.exists()) { @@ -165,13 +183,13 @@ public class WizardCommand implements Command { if (askBoolean("Prompts directory is: " + promptsDir.getAbsolutePath() + " . Is this OK?", true)) { break; } - config.setPromptsDirectory(null); + configuration.setPromptsDirectory(null); } } // 2.4 llama_cli_path while (true) { - File llamaCli = config.getLlamaCliPath(); + File llamaCli = configuration.getLlamaCliPath(); boolean valid = (llamaCli != null && llamaCli.isFile() && llamaCli.exists()); if (!valid) { System.out.println("The 'llama-cli' executable path is missing or invalid."); @@ -181,17 +199,17 @@ public class WizardCommand implements Command { defaultVal ); llamaCli = new File(userInput); - config.setLlamaCliPath(llamaCli); + configuration.setLlamaCliPath(llamaCli); } else { if (askBoolean("Llama-cli path is: " + llamaCli.getAbsolutePath() + " . Is this OK?", true)) { break; } - config.setLlamaCliPath(null); + configuration.setLlamaCliPath(null); } } // 2.5 default_temperature - float oldTemp = config.getDefaultTemperature(); + float oldTemp = configuration.getDefaultTemperature(); if (oldTemp < 0.0f || oldTemp > 3.0f) { oldTemp = 0.7f; } @@ -202,10 +220,10 @@ public class WizardCommand implements Command { ); - config.setDefaultTemperature(newTemp); + configuration.setDefaultTemperature(newTemp); // 2.6 thread_count - int oldThreadCount = config.getThreadCount(); + int oldThreadCount = configuration.getThreadCount(); if (oldThreadCount < 1) { oldThreadCount = 6; } @@ -214,10 +232,10 @@ public class WizardCommand implements Command { oldThreadCount, 1, null, false ); - config.setThreadCount(newThreadCount); + configuration.setThreadCount(newThreadCount); // 2.7 batch_thread_count - int oldBatchThreadCount = config.getBatchThreadCount(); + int oldBatchThreadCount = configuration.getBatchThreadCount(); if (oldBatchThreadCount < 1) { oldBatchThreadCount = 10; } @@ -226,7 +244,7 @@ public class WizardCommand implements Command { oldBatchThreadCount, 1, null, false ); - config.setBatchThreadCount(newBatchCount); + configuration.setBatchThreadCount(newBatchCount); } /** @@ -236,11 +254,11 @@ public class WizardCommand implements Command { private void checkMailDirectory() { System.out.println("Älyverkko-cli uses a 'mail' directory to store and discover tasks that it has to solve. " + "AI generated solutions are appended at the end of the task file. " + - "It should be a directory that you can write to.\n\nCurrent mail directory is: " + describeValue(config.getMailDirectory())); + "It should be a directory that you can write to.\n\nCurrent mail directory is: " + describeValue(configuration.getMailDirectory())); // 2.1 mail_directory while (true) { - File mailDir = config.getMailDirectory(); + File mailDir = configuration.getMailDirectory(); if (mailDir == null) { String userInput = askString( "Enter the desired mail directory path", @@ -253,7 +271,7 @@ public class WizardCommand implements Command { , null, null, false); mailDir = new File(userInput); } - config.setMailDirectory(mailDir); + configuration.setMailDirectory(mailDir); // Attempt to create if not exist if (!mailDir.exists()) { @@ -278,8 +296,8 @@ public class WizardCommand implements Command { * Then let user manually add more if desired. */ private void fixModelEntries() { - if (config.getModels() == null) { - config.setModels(new ArrayList<>()); + if (configuration.getModels() == null) { + configuration.setModels(new ArrayList<>()); } // Remove references to nonexistent model files @@ -287,7 +305,7 @@ public class WizardCommand implements Command { // Offer to autodiscover new model files if (askBoolean("\nAttempt to autodiscover new .gguf model files in the models directory?", true)) { - discoverAndSuggestNewModels(config.getModelsDirectory(), config.getModels()); + discoverAndSuggestNewModels(configuration.getModelsDirectory(), configuration.getModels()); } // Let user manually add new models @@ -299,10 +317,10 @@ public class WizardCommand implements Command { * let user decide whether to remove that model or keep it (perhaps to fix the path). */ private void removeDeadModelReferences() { - Iterator it = config.getModels().iterator(); + Iterator it = configuration.getModels().iterator(); while (it.hasNext()) { ConfigurationModel cm = it.next(); - File f = new File(config.getModelsDirectory(), cm.getFilesystemPath()); + File f = new File(configuration.getModelsDirectory(), cm.getFilesystemPath()); if (!f.exists()) { System.out.println("\nModel with alias '" + cm.getAlias() + "' references missing file: " + f.getAbsolutePath()); @@ -386,7 +404,7 @@ public class WizardCommand implements Command { ); // Check for duplicates - boolean alreadyExists = config.getModels().stream() + boolean alreadyExists = configuration.getModels().stream() .anyMatch(m -> m.getAlias().equals(alias)); if (alreadyExists) { printRedMessageToConsole("Model with alias '" + alias + "' already exists! Skipping."); @@ -399,7 +417,7 @@ public class WizardCommand implements Command { model.setContextSizeTokens(contextSize); model.setEndOfTextMarker(null); - config.getModels().add(model); + configuration.getModels().add(model); } } @@ -410,14 +428,6 @@ public class WizardCommand implements Command { private void trySaveConfiguration(File configFile) { boolean alreadyExists = Files.exists(configFile.toPath()); - if (alreadyExists && !forceOverwriteOption.isPresent()) { - boolean userOk = askBoolean("\nConfiguration file already exists. Overwrite it?", true); - if (!userOk) { - System.out.println("Not overwriting. If you want to do so, run again with --force."); - return; - } - } - try { Files.createDirectories(configFile.toPath().getParent()); try (BufferedWriter writer = Files.newBufferedWriter( @@ -425,7 +435,7 @@ public class WizardCommand implements Command { StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING )) { - new ObjectMapper(new YAMLFactory()).writeValue(writer, config); + new ObjectMapper(new YAMLFactory()).writeValue(writer, configuration); } System.out.println("\nConfiguration saved to: " + configFile.toPath()); } catch (IOException e) { @@ -442,37 +452,37 @@ public class WizardCommand implements Command { boolean allOk = true; // Check mail dir - if (config.getMailDirectory() == null || !config.getMailDirectory().isDirectory()) { - System.err.println("Mail directory is invalid: " + config.getMailDirectory()); + if (configuration.getMailDirectory() == null || !configuration.getMailDirectory().isDirectory()) { + System.err.println("Mail directory is invalid: " + configuration.getMailDirectory()); allOk = false; } // Check llama-cli - if (config.getLlamaCliPath() == null || !config.getLlamaCliPath().isFile()) { - System.err.println("llama-cli path is invalid: " + config.getLlamaCliPath()); + if (configuration.getLlamaCliPath() == null || !configuration.getLlamaCliPath().isFile()) { + System.err.println("llama-cli path is invalid: " + configuration.getLlamaCliPath()); allOk = false; } // Check prompts dir - if (config.getPromptsDirectory() == null || !config.getPromptsDirectory().isDirectory()) { - System.err.println("Prompts directory is invalid: " + config.getPromptsDirectory()); + if (configuration.getPromptsDirectory() == null || !configuration.getPromptsDirectory().isDirectory()) { + System.err.println("Prompts directory is invalid: " + configuration.getPromptsDirectory()); allOk = false; } else { - File[] pFiles = config.getPromptsDirectory().listFiles(); + File[] pFiles = configuration.getPromptsDirectory().listFiles(); if (pFiles == null || pFiles.length == 0) { - System.out.println("Warning: No prompt files found in " + config.getPromptsDirectory()); + System.out.println("Warning: No prompt files found in " + configuration.getPromptsDirectory()); // not necessarily fatal, so we won't mark allOk = false } } // Check models - if (config.getModels() == null || config.getModels().isEmpty()) { + if (configuration.getModels() == null || configuration.getModels().isEmpty()) { System.err.println("No models are defined in the configuration."); allOk = false; } else { // Check each model file - for (ConfigurationModel m : config.getModels()) { - File f = new File(config.getModelsDirectory(), m.getFilesystemPath()); + for (ConfigurationModel m : configuration.getModels()) { + File f = new File(configuration.getModelsDirectory(), m.getFilesystemPath()); if (!f.exists()) { System.err.println("Model alias '" + m.getAlias() + "' references missing file: " + f.getAbsolutePath()); @@ -482,18 +492,18 @@ public class WizardCommand implements Command { } // Check temperature - if (config.getDefaultTemperature() < 0 || config.getDefaultTemperature() > 3) { - System.err.println("Default temperature must be between 0 and 3. Found: " + config.getDefaultTemperature()); + if (configuration.getDefaultTemperature() < 0 || configuration.getDefaultTemperature() > 3) { + System.err.println("Default temperature must be between 0 and 3. Found: " + configuration.getDefaultTemperature()); allOk = false; } - if (config.getThreadCount() < 1) { - System.err.println("thread_count must be >= 1. Found: " + config.getThreadCount()); + if (configuration.getThreadCount() < 1) { + System.err.println("thread_count must be >= 1. Found: " + configuration.getThreadCount()); allOk = false; } - if (config.getBatchThreadCount() < 1) { - System.err.println("batch_thread_count must be >= 1. Found: " + config.getBatchThreadCount()); + if (configuration.getBatchThreadCount() < 1) { + System.err.println("batch_thread_count must be >= 1. Found: " + configuration.getBatchThreadCount()); allOk = false; } diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/mail_correspondant/MailCorrespondentCommand.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/mail_correspondant/MailCorrespondentCommand.java index c83f2d4..469f626 100644 --- a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/mail_correspondant/MailCorrespondentCommand.java +++ b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/mail_correspondant/MailCorrespondentCommand.java @@ -69,7 +69,7 @@ public class MailCorrespondentCommand implements Command { * @return the name of this command, i.e., "mail". */ @Override - public String getName() { + public String getCommandName() { return "mail"; } @@ -83,7 +83,7 @@ public class MailCorrespondentCommand implements Command { * @throws InterruptedException if the WatchService is interrupted. */ @Override - public void execute(String[] cliArguments) throws IOException, InterruptedException { + public void executeCommand(String[] cliArguments) throws IOException, InterruptedException { if (!parser.parse(cliArguments)) { System.out.println("Failed to parse commandline arguments"); parser.showHelp(); -- 2.20.1