Wizard usability improvements better-wizard
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Thu, 10 Apr 2025 20:35:23 +0000 (23:35 +0300)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Thu, 10 Apr 2025 20:35:23 +0000 (23:35 +0300)
src/main/java/eu/svjatoslav/alyverkko_cli/Command.java
src/main/java/eu/svjatoslav/alyverkko_cli/Main.java
src/main/java/eu/svjatoslav/alyverkko_cli/commands/JoinFilesCommand.java
src/main/java/eu/svjatoslav/alyverkko_cli/commands/ListModelsCommand.java
src/main/java/eu/svjatoslav/alyverkko_cli/commands/WizardCommand.java
src/main/java/eu/svjatoslav/alyverkko_cli/commands/mail_correspondant/MailCorrespondentCommand.java

index 30748ca..dfd9b14 100644 (file)
@@ -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;
 }
index ed81cfb..48bba6c 100644 (file)
@@ -58,7 +58,7 @@ public class Main {
 
         String commandName = args[0].toLowerCase();
         Optional<Command> 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()));
     }
 }
index 1e644ed..072bb3a 100644 (file)
@@ -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");
index 56128a6..5078177 100644 (file)
@@ -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");
index b4832c4..5b1ff9d 100644 (file)
@@ -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<ConfigurationModel> it = config.getModels().iterator();
+        Iterator<ConfigurationModel> 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;
         }
 
index c83f2d4..469f626 100644 (file)
@@ -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();