001/* 002 * Copyright 2025 devteam@scivics-lab.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, 011 * software distributed under the License is distributed on an 012 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 013 * either express or implied. See the License for the 014 * specific language governing permissions and limitations 015 * under the License. 016 */ 017 018package com.scivicslab.actoriac.cli; 019 020import java.io.File; 021import java.io.IOException; 022import java.io.InputStream; 023import java.nio.file.Files; 024import java.nio.file.Path; 025import java.util.Arrays; 026import java.util.Comparator; 027import java.util.LinkedHashMap; 028import java.util.List; 029import java.util.Map; 030import java.util.concurrent.Callable; 031import java.util.logging.LogManager; 032import java.util.stream.Collectors; 033import java.util.stream.Stream; 034 035import picocli.CommandLine; 036import picocli.CommandLine.Command; 037import picocli.CommandLine.Option; 038 039/** 040 * Main command-line interface for actor-IaC. 041 * 042 * <p>This is the entry point for all actor-IaC commands. Use subcommands to 043 * execute specific operations:</p> 044 * 045 * <h2>Subcommands</h2> 046 * <ul> 047 * <li>{@code run} - Execute workflows</li> 048 * <li>{@code list} - List available workflows</li> 049 * <li>{@code describe} - Describe workflow structure</li> 050 * <li>{@code logs} - Query execution logs</li> 051 * <li>{@code log-server} - Start H2 log server</li> 052 * </ul> 053 * 054 * <h2>Usage Examples</h2> 055 * <pre> 056 * actor-iac run -d ./workflows -w deploy 057 * actor-iac list -d ./workflows 058 * actor-iac logs --db ./logs --list 059 * actor-iac log-server --db ./logs 060 * </pre> 061 * 062 * @author devteam@scivics-lab.com 063 * @since 2.7.0 064 */ 065@Command( 066 name = "actor-iac", 067 mixinStandardHelpOptions = true, 068 version = "actor-IaC 2.10.0", 069 description = "Infrastructure as Code workflow automation tool.", 070 subcommands = { 071 RunCLI.class, 072 ListWorkflowsCLI.class, 073 DescribeCLI.class, 074 LogsCLI.class, 075 LogServerCLI.class, 076 MergeLogsCLI.class 077 } 078) 079public class WorkflowCLI implements Callable<Integer> { 080 081 /** 082 * Main entry point. 083 * 084 * @param args command line arguments 085 */ 086 public static void main(String[] args) { 087 // Load logging configuration from resources 088 try (InputStream is = WorkflowCLI.class.getResourceAsStream("/logging.properties")) { 089 if (is != null) { 090 LogManager.getLogManager().readConfiguration(is); 091 } 092 } catch (IOException e) { 093 System.err.println("Warning: Failed to load logging.properties: " + e.getMessage()); 094 } 095 096 int exitCode = new CommandLine(new WorkflowCLI()).execute(args); 097 System.exit(exitCode); 098 } 099 100 /** 101 * Called when no subcommand is specified. 102 * Shows help message. 103 */ 104 @Override 105 public Integer call() { 106 System.out.println("actor-IaC - Infrastructure as Code workflow automation tool"); 107 System.out.println(); 108 System.out.println("Usage: actor-iac <command> [options]"); 109 System.out.println(); 110 System.out.println("Commands:"); 111 System.out.println(" run Execute a workflow"); 112 System.out.println(" list List available workflows"); 113 System.out.println(" describe Describe workflow structure"); 114 System.out.println(" log-search Search execution logs"); 115 System.out.println(" log-serve Start H2 log server for centralized logging"); 116 System.out.println(" log-merge Merge scattered log databases into one"); 117 System.out.println(); 118 System.out.println("Examples:"); 119 System.out.println(" actor-iac run -d ./workflows -w deploy"); 120 System.out.println(" actor-iac run -d ./workflows -w deploy -i inventory.ini -g webservers"); 121 System.out.println(" actor-iac list -d ./workflows"); 122 System.out.println(" actor-iac log-search --db ./logs --list"); 123 System.out.println(" actor-iac log-serve --db ./logs/shared"); 124 System.out.println(" actor-iac log-merge --scan ./workflows --target ./logs/merged"); 125 System.out.println(); 126 System.out.println("Use 'actor-iac <command> --help' for more information about a command."); 127 return 0; 128 } 129} 130 131/** 132 * Subcommand to list workflows discovered under a directory. 133 */ 134@Command( 135 name = "list", 136 mixinStandardHelpOptions = true, 137 version = "actor-IaC list 2.10.0", 138 description = "List workflows discovered under --dir." 139) 140class ListWorkflowsCLI implements Callable<Integer> { 141 142 @Option( 143 names = {"-d", "--dir"}, 144 required = true, 145 description = "Directory containing workflow files (searched recursively)" 146 ) 147 private File workflowDir; 148 149 @Override 150 public Integer call() { 151 if (!workflowDir.isDirectory()) { 152 System.err.println("Not a directory: " + workflowDir); 153 return 1; 154 } 155 156 List<WorkflowDisplay> displays = scanWorkflowsForDisplay(workflowDir); 157 if (displays.isEmpty()) { 158 System.out.println("No workflow files found under " + workflowDir.getAbsolutePath()); 159 return 0; 160 } 161 162 System.out.println("Available workflows (directory: " + workflowDir.getAbsolutePath() + ")"); 163 System.out.println("-".repeat(90)); 164 System.out.printf("%-4s %-25s %-35s %s%n", "#", "File (-w)", "Path", "Workflow Name (in logs)"); 165 System.out.println("-".repeat(90)); 166 int index = 1; 167 for (WorkflowDisplay display : displays) { 168 String workflowName = display.workflowName() != null ? display.workflowName() : "(no name)"; 169 System.out.printf("%2d. %-25s %-35s %s%n", 170 index++, display.baseName(), display.relativePath(), workflowName); 171 } 172 System.out.println("-".repeat(90)); 173 System.out.println("Use 'actor-iac run -d " + workflowDir + " -w <File>' to execute a workflow."); 174 return 0; 175 } 176 177 private static List<WorkflowDisplay> scanWorkflowsForDisplay(File directory) { 178 if (directory == null) { 179 return List.of(); 180 } 181 182 try (Stream<Path> paths = Files.walk(directory.toPath())) { 183 Map<String, File> uniqueFiles = new LinkedHashMap<>(); 184 paths.filter(Files::isRegularFile) 185 .filter(path -> { 186 String name = path.getFileName().toString().toLowerCase(); 187 return name.endsWith(".yaml") || name.endsWith(".yml") 188 || name.endsWith(".json") || name.endsWith(".xml"); 189 }) 190 .forEach(path -> uniqueFiles.putIfAbsent(path.toFile().getAbsolutePath(), path.toFile())); 191 192 Path basePath = directory.toPath(); 193 return uniqueFiles.values().stream() 194 .map(file -> new WorkflowDisplay( 195 getBaseName(file.getName()), 196 relativize(basePath, file.toPath()), 197 extractWorkflowName(file))) 198 .sorted(Comparator.comparing(WorkflowDisplay::baseName, String.CASE_INSENSITIVE_ORDER)) 199 .collect(Collectors.toList()); 200 } catch (IOException e) { 201 System.err.println("Failed to scan workflows: " + e.getMessage()); 202 return List.of(); 203 } 204 } 205 206 private static String getBaseName(String fileName) { 207 int dotIndex = fileName.lastIndexOf('.'); 208 if (dotIndex > 0) { 209 return fileName.substring(0, dotIndex); 210 } 211 return fileName; 212 } 213 214 private static String relativize(Path base, Path target) { 215 try { 216 return base.relativize(target).toString(); 217 } catch (IllegalArgumentException e) { 218 return target.toAbsolutePath().toString(); 219 } 220 } 221 222 private static String extractWorkflowName(File file) { 223 String fileName = file.getName().toLowerCase(); 224 if (fileName.endsWith(".yaml") || fileName.endsWith(".yml")) { 225 return extractNameFromYaml(file); 226 } else if (fileName.endsWith(".json")) { 227 return extractNameFromJson(file); 228 } 229 return null; 230 } 231 232 private static String extractNameFromYaml(File file) { 233 try (java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(file))) { 234 String line; 235 while ((line = reader.readLine()) != null) { 236 line = line.trim(); 237 if (line.startsWith("name:")) { 238 String value = line.substring(5).trim(); 239 if ((value.startsWith("\"") && value.endsWith("\"")) || 240 (value.startsWith("'") && value.endsWith("'"))) { 241 value = value.substring(1, value.length() - 1); 242 } 243 return value.isEmpty() ? null : value; 244 } 245 if (line.startsWith("steps:") || line.startsWith("vertices:")) { 246 break; 247 } 248 } 249 } catch (IOException e) { 250 // Ignore 251 } 252 return null; 253 } 254 255 private static String extractNameFromJson(File file) { 256 try (java.io.FileReader reader = new java.io.FileReader(file)) { 257 org.json.JSONTokener tokener = new org.json.JSONTokener(reader); 258 org.json.JSONObject json = new org.json.JSONObject(tokener); 259 return json.optString("name", null); 260 } catch (Exception e) { 261 // Ignore 262 } 263 return null; 264 } 265 266 private static record WorkflowDisplay(String baseName, String relativePath, String workflowName) {} 267}