001/* 002 * Copyright 2025 devteam@scivicslab.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.FileInputStream; 022import java.io.InputStream; 023import java.util.List; 024import java.util.Map; 025import java.util.concurrent.Callable; 026 027import com.scivicslab.pojoactor.workflow.kustomize.WorkflowKustomizer; 028 029import picocli.CommandLine.Command; 030import picocli.CommandLine.Option; 031 032/** 033 * CLI subcommand for displaying workflow descriptions. 034 * 035 * <p>Usage examples:</p> 036 * <pre> 037 * # Show workflow description only 038 * actor-iac describe -w sysinfo/main-collect-sysinfo.yaml 039 * 040 * # Show workflow description with step descriptions 041 * actor-iac describe -w sysinfo/main-collect-sysinfo.yaml --steps 042 * 043 * # With overlay 044 * actor-iac describe -w sysinfo/main-collect-sysinfo.yaml -o ./overlays/env --steps 045 * </pre> 046 * 047 * @author devteam@scivicslab.com 048 * @since 2.10.0 049 */ 050@Command( 051 name = "describe", 052 mixinStandardHelpOptions = true, 053 versionProvider = VersionProvider.class, 054 description = "Display workflow and step descriptions." 055) 056public class DescribeCLI implements Callable<Integer> { 057 058 @Option( 059 names = {"-d", "--dir"}, 060 description = "Base directory. Defaults to current directory.", 061 defaultValue = "." 062 ) 063 private File baseDir; 064 065 @Option( 066 names = {"-w", "--workflow"}, 067 description = "Workflow file path relative to -d (required)", 068 required = true 069 ) 070 private String workflowPath; 071 072 @Option( 073 names = {"-o", "--overlay"}, 074 description = "Overlay directory containing overlay-conf.yaml" 075 ) 076 private File overlayDir; 077 078 @Option( 079 names = {"--steps", "--notes"}, 080 description = "Also display note/description of each step" 081 ) 082 private boolean showSteps; 083 084 @Override 085 public Integer call() { 086 // Resolve workflow file (try extensions if needed) 087 File resolvedFile = resolveWorkflowFile(new File(baseDir, workflowPath)); 088 if (resolvedFile == null || !resolvedFile.isFile()) { 089 System.err.println("Workflow file not found: " + workflowPath); 090 return 1; 091 } 092 093 // Load YAML (with overlay if specified) 094 Map<String, Object> yaml; 095 if (overlayDir != null) { 096 yaml = loadYamlWithOverlay(resolvedFile); 097 } else { 098 yaml = loadYamlFile(resolvedFile); 099 } 100 101 if (yaml == null) { 102 System.err.println("Failed to load workflow: " + resolvedFile); 103 return 1; 104 } 105 106 // Print workflow description 107 printWorkflowDescription(resolvedFile, yaml); 108 109 return 0; 110 } 111 112 /** 113 * Resolves the workflow file, trying extensions if the file doesn't exist. 114 */ 115 private File resolveWorkflowFile(File file) { 116 if (file.isFile()) { 117 return file; 118 } 119 // Try adding extensions 120 String[] extensions = {".yaml", ".yml", ".json", ".xml"}; 121 for (String ext : extensions) { 122 File candidate = new File(file.getPath() + ext); 123 if (candidate.isFile()) { 124 return candidate; 125 } 126 } 127 return null; 128 } 129 130 /** 131 * Prints workflow description. 132 */ 133 @SuppressWarnings("unchecked") 134 private void printWorkflowDescription(File file, Map<String, Object> yaml) { 135 String name = (String) yaml.getOrDefault("name", "(unnamed)"); 136 String description = (String) yaml.get("description"); 137 138 System.out.println("Workflow: " + name); 139 System.out.println("File: " + file.getAbsolutePath()); 140 if (overlayDir != null) { 141 System.out.println("Overlay: " + overlayDir.getAbsolutePath()); 142 } 143 System.out.println(); 144 145 // Workflow-level description 146 System.out.println("Description:"); 147 if (description != null && !description.isBlank()) { 148 for (String line : description.split("\n")) { 149 System.out.println(" " + line); 150 } 151 } else { 152 System.out.println(" (no description)"); 153 } 154 155 // Step descriptions (if --steps flag is set) 156 if (showSteps) { 157 System.out.println(); 158 System.out.println("Steps:"); 159 160 List<Map<String, Object>> steps = (List<Map<String, Object>>) yaml.get("steps"); 161 if (steps == null || steps.isEmpty()) { 162 System.out.println(" (no steps defined)"); 163 return; 164 } 165 166 for (Map<String, Object> step : steps) { 167 List<String> states = (List<String>) step.get("states"); 168 String label = (String) step.get("label"); 169 String stepNote = (String) step.get("note"); 170 171 String stateTransition = (states != null && states.size() >= 2) 172 ? states.get(0) + " -> " + states.get(1) 173 : "?"; 174 175 String displayName = (label != null) ? label : "(unnamed)"; 176 177 System.out.println(); 178 System.out.println(" [" + stateTransition + "] " + displayName); 179 if (stepNote != null && !stepNote.isBlank()) { 180 for (String line : stepNote.split("\n")) { 181 System.out.println(" " + line); 182 } 183 } 184 } 185 } 186 } 187 188 /** 189 * Loads a YAML file. 190 */ 191 private Map<String, Object> loadYamlFile(File file) { 192 try (InputStream is = new FileInputStream(file)) { 193 org.yaml.snakeyaml.Yaml yaml = new org.yaml.snakeyaml.Yaml(); 194 return yaml.load(is); 195 } catch (Exception e) { 196 System.err.println("Failed to load YAML file: " + file + " - " + e.getMessage()); 197 return null; 198 } 199 } 200 201 /** 202 * Loads a YAML file with overlay applied. 203 */ 204 private Map<String, Object> loadYamlWithOverlay(File workflowFile) { 205 try { 206 WorkflowKustomizer kustomizer = new WorkflowKustomizer(); 207 Map<String, Map<String, Object>> workflows = kustomizer.build(overlayDir.toPath()); 208 209 String targetName = workflowFile.getName(); 210 for (Map.Entry<String, Map<String, Object>> entry : workflows.entrySet()) { 211 if (entry.getKey().equals(targetName)) { 212 return entry.getValue(); 213 } 214 } 215 216 // Fall back to raw file 217 return loadYamlFile(workflowFile); 218 } catch (Exception e) { 219 System.err.println("Failed to apply overlay, loading raw file: " + e.getMessage()); 220 return loadYamlFile(workflowFile); 221 } 222 } 223 224}