Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
274aeb6
Initial plan
Copilot Aug 25, 2025
734664e
Initial analysis and plan for implementing minimalist run option
Copilot Aug 25, 2025
20b9ce4
Implement minimalist run option with do/run commands and aliases
Copilot Aug 25, 2025
2d737db
Fix help text formatting for do command
Copilot Aug 25, 2025
99a247e
Address PR feedback: remove run command, rename scripts to actions, c…
Copilot Aug 25, 2025
bb07e68
chore: Add comprehensive GitHub Copilot instructions for jpm repository
Copilot Aug 25, 2025
fad7e80
Initial plan
Copilot Aug 25, 2025
f4533dd
Address PR feedback: fix formatting and change 'compile' to 'build' a…
Copilot Aug 25, 2025
e2028b5
Add --list/-l flag to 'do' command to list available actions
Copilot Aug 25, 2025
ef72e32
Optimize 'do' command to only get classpath when ${deps} variable is …
Copilot Aug 25, 2025
f2ca4ff
Add comprehensive JUnit tests for new action execution features
Copilot Aug 25, 2025
e1873c1
Fix AppInfo path resolution and most unit test failures
Copilot Aug 25, 2025
9d3be8b
Fix MainIntegrationTest hanging issues by replacing Main.main() calls…
Copilot Aug 25, 2025
b2604ed
Add Windows to CI workflow matrix to build and test on multiple platf…
Copilot Aug 25, 2025
1114385
Fix Windows test failures by handling cross-platform path differences
Copilot Aug 25, 2025
803171d
Remove code duplication by making isWindows() public and fix app.yml …
Copilot Aug 25, 2025
b48c27d
Change variable substitution from ${deps} to {{deps}} and update copi…
Copilot Aug 25, 2025
fcacf03
Merge branch 'main' into copilot/fix-43
quintesse Aug 26, 2025
9337631
Fix corrupted channel issue in Maven Surefire tests by removing unnec…
Copilot Aug 26, 2025
b99a5a3
Fix cross-platform compatibility for test commands by using platform-…
Copilot Aug 26, 2025
2f60ed5
Fixed hanging test
quintesse Aug 26, 2025
b675734
Refactor 'do' command to follow Main/Jpm separation pattern by moving…
Copilot Aug 26, 2025
0099220
Fixed stupid AI code
quintesse Aug 26, 2025
a9fd7ce
Added {:} and {/}
quintesse Aug 26, 2025
1f1da03
DOn't parse command, just pass it as-is to sh or cmd
quintesse Aug 26, 2025
cfa8bde
Allow executing multiple commands
quintesse Aug 27, 2025
6e29589
Fix alias commands to accept and pass through unknown options using @…
Copilot Aug 27, 2025
8958d74
updated test
quintesse Aug 27, 2025
ec07563
Fix argument ordering in alias commands by using only @Unmatched anno…
Copilot Aug 27, 2025
63c13a3
Replace JUnit assertions with AssertJ fluent assertions across all te…
Copilot Aug 27, 2025
1a2632a
minor refactor
quintesse Aug 28, 2025
c652724
Added complete path replacement in actions
quintesse Aug 28, 2025
3a298b8
Add comprehensive Actions section to README explaining do command and…
Copilot Aug 28, 2025
772c5da
minor README change
quintesse Aug 28, 2025
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
Empty file modified mvnw
100644 → 100755
Empty file.
64 changes: 63 additions & 1 deletion src/main/java/org/codejive/jpm/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
import java.util.*;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import org.codejive.jpm.json.AppInfo;
import org.codejive.jpm.util.SyncStats;
import org.codejive.jpm.util.ScriptUtils;
import org.codejive.jpm.util.Version;
import org.jline.consoleui.elements.InputValue;
import org.jline.consoleui.elements.ListChoice;
Expand Down Expand Up @@ -45,7 +47,8 @@
Main.Copy.class,
Main.Search.class,
Main.Install.class,
Main.PrintPath.class
Main.PrintPath.class,
Main.Do.class
})
public class Main {

Expand Down Expand Up @@ -311,6 +314,52 @@ public Integer call() throws Exception {
}
}

@Command(
name = "do",
description =
"Executes an action command defined in the app.yml file. Actions can use variable substitution for classpath.\n\n"
+ "Example:\n jpm do build\n jpm do test\n")
static class Do implements Callable<Integer> {
@Mixin CopyMixin copyMixin;

@Parameters(
paramLabel = "actionName",
description = "Name of the action to execute as defined in app.yml",
arity = "1")
private String actionName;

@Override
public Integer call() throws Exception {
AppInfo appInfo = AppInfo.read();
String command = appInfo.getAction(actionName);

if (command == null) {
System.err.println("Action '" + actionName + "' not found in app.yml");
if (!appInfo.getActionNames().isEmpty()) {
System.err.println("Available actions: " + String.join(", ", appInfo.getActionNames()));
}
return 1;
}

// Get the classpath for variable substitution using app.yml dependencies
List<Path> classpath = Collections.emptyList();
try {
classpath = Jpm.builder()
.directory(copyMixin.directory)
.noLinks(copyMixin.noLinks)
.build()
.path(new String[0]); // Empty array means use dependencies from app.yml
} catch (Exception e) {
// If we can't get the classpath, continue with empty list
System.err.println("Warning: Could not resolve classpath: " + e.getMessage());
}

return ScriptUtils.executeScript(command, classpath);
}
}



static class CopyMixin {
@Option(
names = {"-d", "--directory"},
Expand Down Expand Up @@ -372,6 +421,19 @@ public static void main(String... args) {
"Running 'jpm search --interactive', try 'jpm --help' for more options");
args = new String[] {"search", "--interactive"};
}

// Handle common aliases
if (args.length > 0) {
String firstArg = args[0];
if ("compile".equals(firstArg) || "test".equals(firstArg) || "run".equals(firstArg)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please change "compile" to "build"

// Convert "jpm compile", "jpm test", "jpm run" to "jpm do <command>"
String[] newArgs = new String[args.length + 1];
newArgs[0] = "do";
System.arraycopy(args, 0, newArgs, 1, args.length);
args = newArgs;
}
}

new CommandLine(new Main()).execute(args);
}
}
29 changes: 29 additions & 0 deletions src/main/java/org/codejive/jpm/json/AppInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
public class AppInfo {
private Map<String, Object> yaml = new TreeMap<>();
public Map<String, String> dependencies = new TreeMap<>();
public Map<String, String> actions = new TreeMap<>();

/** The official name of the app.yml file. */
public static final String APP_INFO_FILE = "app.yml";
Expand All @@ -33,6 +34,25 @@ public String[] getDependencyGAVs() {
.toArray(String[]::new);
}

/**
* Returns the action command for the given action name.
*
* @param actionName The name of the action
* @return The action command or null if not found
*/
public String getAction(String actionName) {
return actions.get(actionName);
}

/**
* Returns all available action names.
*
* @return A set of action names
*/
public java.util.Set<String> getActionNames() {
return actions.keySet();
}

/**
* Reads the app.yml file in the current directory and returns its content as an AppInfo object.
*
Expand All @@ -58,6 +78,14 @@ public static AppInfo read() throws IOException {
appInfo.dependencies.put(entry.getKey(), entry.getValue().toString());
}
}
// Parse actions section
if (appInfo.yaml.containsKey("actions")
&& appInfo.yaml.get("actions") instanceof Map) {
Map<String, Object> actions = (Map<String, Object>) appInfo.yaml.get("actions");
for (Map.Entry<String, Object> entry : actions.entrySet()) {
appInfo.actions.put(entry.getKey(), entry.getValue().toString());
}
}
return appInfo;
}

Expand All @@ -76,6 +104,7 @@ public static void write(AppInfo appInfo) throws IOException {
Yaml yaml = new Yaml(dopts);
// WARNING awful code ahead
appInfo.yaml.put("dependencies", (Map<String, Object>) (Map) appInfo.dependencies);
appInfo.yaml.put("actions", (Map<String, Object>) (Map) appInfo.actions);
yaml.dump(appInfo.yaml, out);
}
}
Expand Down
115 changes: 115 additions & 0 deletions src/main/java/org/codejive/jpm/util/ScriptUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package org.codejive.jpm.util;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;

/** Utility class for executing scripts with path conversion and variable substitution. */
public class ScriptUtils {

/**
* Executes a script command with variable substitution and path conversion.
*
* @param command The command to execute
* @param classpath The classpath to use for ${deps} substitution
* @return The exit code of the executed command
* @throws IOException if an error occurred during execution
* @throws InterruptedException if the execution was interrupted
*/
public static int executeScript(String command, List<Path> classpath)
throws IOException, InterruptedException {
String processedCommand = processCommand(command, classpath);

// Split command into tokens for ProcessBuilder
String[] commandTokens = parseCommand(processedCommand);

ProcessBuilder pb = new ProcessBuilder(commandTokens);
pb.inheritIO(); // Connect to current process's stdin/stdout/stderr
Process process = pb.start();

return process.waitFor();
}

/**
* Processes a command by performing variable substitution and path conversion.
*
* @param command The raw command
* @param classpath The classpath to use for ${deps} substitution
* @return The processed command
*/
private static String processCommand(String command, List<Path> classpath) {
String result = command;

// Substitute ${deps} with the classpath
if (result.contains("${deps}")) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please change ${deps} to {{deps}} and make sure to update other references and docs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed all variable substitution from ${deps} to {{deps}} throughout the codebase, including:

  • ScriptUtils.java: Updated substitution logic and documentation
  • Main.java: Updated condition check for variable presence
  • All test files: Updated test cases and expectations
  • All comments and JavaDoc: Updated references

All 35 tests still pass with the new syntax. See commit b48c27d.

String classpathStr = "";
if (classpath != null && !classpath.isEmpty()) {
classpathStr = classpath.stream()
.map(Path::toString)
.collect(Collectors.joining(File.pathSeparator));
}
result = result.replace("${deps}", classpathStr);
}

// Convert Unix-style paths to Windows if needed
if (isWindows()) {
result = convertPathsForWindows(result);
}

return result;
}

/**
* Converts Unix-style paths to Windows format.
* This is a simple heuristic that looks for patterns like "deps/*" and converts them.
*/
private static String convertPathsForWindows(String command) {
// Convert forward slashes in path-like patterns to backslashes
// This is a simple heuristic - in a real implementation you might want more sophisticated logic
return command.replaceAll("([a-zA-Z0-9_.-]+)/\\*", "$1\\\\*")
.replaceAll("([a-zA-Z0-9_.-]+)/([a-zA-Z0-9_.-]+)", "$1\\\\$2");
}

/**
* Parses a command string into tokens for ProcessBuilder.
* This is a simple implementation that splits on spaces while respecting quotes.
*/
private static String[] parseCommand(String command) {
// Simple parsing - for a full implementation you'd want proper shell parsing
java.util.List<String> tokens = new java.util.ArrayList<>();
boolean inQuotes = false;
StringBuilder currentToken = new StringBuilder();

for (char c : command.toCharArray()) {
if (c == '"' || c == '\'') {
inQuotes = !inQuotes;
} else if (c == ' ' && !inQuotes) {
if (currentToken.length() > 0) {
tokens.add(currentToken.toString());
currentToken.setLength(0);
}
} else {
currentToken.append(c);
}
}

if (currentToken.length() > 0) {
tokens.add(currentToken.toString());
}

return tokens.toArray(new String[0]);
}

/**
* Checks if the current operating system is Windows.
*/
private static boolean isWindows() {
String os = System.getProperty("os.name")
.toLowerCase(Locale.ENGLISH)
.replaceAll("[^a-z0-9]+", "");
return os.startsWith("win");
}
}
Loading