From bad291b31c9300c1fc7302f1a126d479beebe2dc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Apr 2026 14:33:27 +0000 Subject: [PATCH 01/14] Add install-skill CLI tool with picocli Implements a CLI that installs skills deployed with the skills-jar-maven-plugin. Usage: install-skill [-r ] [-d ] Creates a temporary Maven project, resolves the skill artifact via the skills-jar:install goal, and copies installed skills to the target directory. https://claude.ai/code/session_019NWGYeNmxc8gyUBF9Y1cqF --- pom.xml | 69 +++ .../installskill/InstallSkillCommand.java | 392 ++++++++++++++++++ 2 files changed, 461 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/ca/weblite/installskill/InstallSkillCommand.java diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..61be7df --- /dev/null +++ b/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + ca.weblite + install-skill-cli + 1.0.0-SNAPSHOT + jar + + Install Skill CLI + CLI tool for installing skills deployed with the skills-jar-maven-plugin + + + UTF-8 + 11 + 4.7.5 + + + + + info.picocli + picocli + ${picocli.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + + info.picocli + picocli-codegen + ${picocli.version} + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + + shade + + + + + ca.weblite.installskill.InstallSkillCommand + + + false + + + + + + + diff --git a/src/main/java/ca/weblite/installskill/InstallSkillCommand.java b/src/main/java/ca/weblite/installskill/InstallSkillCommand.java new file mode 100644 index 0000000..1f1f149 --- /dev/null +++ b/src/main/java/ca/weblite/installskill/InstallSkillCommand.java @@ -0,0 +1,392 @@ +package ca.weblite.installskill; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.stream.Stream; + +/** + * CLI tool that installs skills deployed with the skills-jar-maven-plugin. + * + *

Sets up a temporary Maven project with the specified skill artifact as a dependency, + * runs the skills-jar plugin install goal to resolve and unpack skill bundles, + * then copies the installed skills to the target directory.

+ */ +@Command(name = "install-skill", + mixinStandardHelpOptions = true, + version = "1.0.0", + description = "Install skills deployed with the skills-jar-maven-plugin.") +public class InstallSkillCommand implements Callable { + + private static final String PLUGIN_GROUP_ID = "ca.weblite"; + private static final String PLUGIN_ARTIFACT_ID = "skills-jar-plugin"; + private static final String PLUGIN_VERSION = "0.1.2"; + private static final String DEFAULT_VERSION = "RELEASE"; + + @Parameters(index = "0", + description = "Skill coordinates: groupId:artifactId[:version]. Version defaults to RELEASE (latest release).", + paramLabel = "") + private String coordinates; + + @Option(names = {"-r"}, + description = "Repository URL with optional credentials: [user:pass@]repositoryUrl", + paramLabel = "") + private String repository; + + @Option(names = {"-d"}, + description = "Skills installation directory (default: ./claude/skills).", + paramLabel = "", + defaultValue = "./claude/skills") + private String skillsDir; + + public static void main(String[] args) { + int exitCode = new CommandLine(new InstallSkillCommand()).execute(args); + System.exit(exitCode); + } + + @Override + public Integer call() throws Exception { + // 1. Parse coordinates + String[] parts = coordinates.split(":", -1); + if (parts.length < 2 || parts.length > 3) { + System.err.println("Error: Invalid coordinates format. Expected: groupId:artifactId[:version]"); + return 1; + } + + String groupId = parts[0].trim(); + String artifactId = parts[1].trim(); + String version = (parts.length == 3 && !parts[2].trim().isEmpty()) + ? parts[2].trim() : DEFAULT_VERSION; + + if (groupId.isEmpty() || artifactId.isEmpty()) { + System.err.println("Error: groupId and artifactId must not be empty."); + return 1; + } + + // 2. Parse repository option + String repoUrl = null; + String repoUser = null; + String repoPass = null; + if (repository != null && !repository.isEmpty()) { + String[] repoParts = parseRepository(repository); + repoUrl = repoParts[0]; + repoUser = repoParts[1]; + repoPass = repoParts[2]; + } + + // 3. Verify Maven is available + String mvnCommand = findMavenCommand(); + if (mvnCommand == null) { + System.err.println("Error: Maven (mvn) is not available on the PATH. Please install Maven first."); + return 1; + } + + // 4. Create temp directory + Path tempDir = Files.createTempDirectory("install-skill-"); + System.out.println("Setting up temporary Maven project..."); + + try { + // 5. Generate pom.xml in the temp project + generatePom(tempDir, groupId, artifactId, version, repoUrl); + + // 6. Generate settings.xml if credentials are provided + boolean hasSettings = false; + if (repoUser != null) { + generateSettings(tempDir, repoUser, repoPass); + hasSettings = true; + } + + // 7. Run Maven to install skills + System.out.println("Resolving skill " + groupId + ":" + artifactId + ":" + version + "..."); + int exitCode = runMaven(mvnCommand, tempDir, hasSettings); + if (exitCode != 0) { + System.err.println("Error: Maven execution failed (exit code " + exitCode + ")."); + return exitCode; + } + + // 8. Copy installed skills to target directory + Path installedSkillsDir = tempDir.resolve(".claude").resolve("skills"); + Path targetDir = Paths.get(skillsDir).toAbsolutePath().normalize(); + + if (!Files.exists(installedSkillsDir) || isDirectoryEmpty(installedSkillsDir)) { + System.err.println("Error: No skills were found after installation."); + return 1; + } + + Files.createDirectories(targetDir); + copyDirectory(installedSkillsDir, targetDir); + System.out.println("Skills installed successfully to " + targetDir); + + return 0; + } finally { + // Clean up temp directory + deleteDirectory(tempDir); + } + } + + /** + * Parses a repository string that may contain credentials. + * Format: [user:pass@]repositoryUrl + * + * @return array of [url, user, password] where user/password may be null + */ + static String[] parseRepository(String repo) { + // Look for credentials pattern: text@http:// or text@https:// + int httpAt = repo.indexOf("@http://"); + int httpsAt = repo.indexOf("@https://"); + int atIdx = Math.max(httpAt, httpsAt); + + if (atIdx > 0) { + String credentials = repo.substring(0, atIdx); + String url = repo.substring(atIdx + 1); + int colonIdx = credentials.indexOf(':'); + if (colonIdx > 0) { + return new String[]{url, credentials.substring(0, colonIdx), + credentials.substring(colonIdx + 1)}; + } + } + + return new String[]{repo, null, null}; + } + + /** + * Finds the Maven command (mvn) on the system PATH. + */ + private String findMavenCommand() { + // Try mvn directly + try { + Process p = new ProcessBuilder("mvn", "--version") + .redirectErrorStream(true) + .start(); + p.getInputStream().readAllBytes(); + if (p.waitFor() == 0) { + return "mvn"; + } + } catch (Exception ignored) { + } + + // Try mvn.cmd for Windows + try { + Process p = new ProcessBuilder("mvn.cmd", "--version") + .redirectErrorStream(true) + .start(); + p.getInputStream().readAllBytes(); + if (p.waitFor() == 0) { + return "mvn.cmd"; + } + } catch (Exception ignored) { + } + + return null; + } + + /** + * Generates a temporary pom.xml with the skill artifact as a dependency. + */ + private void generatePom(Path projectDir, String groupId, String artifactId, + String version, String repoUrl) throws IOException { + StringBuilder pom = new StringBuilder(); + pom.append("\n"); + pom.append("\n"); + pom.append(" 4.0.0\n\n"); + pom.append(" install-skill-temp\n"); + pom.append(" install-skill-temp\n"); + pom.append(" 1.0.0\n"); + pom.append(" pom\n\n"); + + // Dependencies + pom.append(" \n"); + pom.append(" \n"); + pom.append(" ").append(escapeXml(groupId)).append("\n"); + pom.append(" ").append(escapeXml(artifactId)).append("\n"); + pom.append(" ").append(escapeXml(version)).append("\n"); + pom.append(" \n"); + pom.append(" \n\n"); + + // Repositories (if custom repo specified) + if (repoUrl != null) { + pom.append(" \n"); + pom.append(" \n"); + pom.append(" custom-repo\n"); + pom.append(" ").append(escapeXml(repoUrl)).append("\n"); + pom.append(" \n"); + pom.append(" true\n"); + pom.append(" \n"); + pom.append(" \n"); + pom.append(" true\n"); + pom.append(" \n"); + pom.append(" \n"); + pom.append(" \n\n"); + + // Also add as plugin repository so the skills-jar-plugin can be resolved + pom.append(" \n"); + pom.append(" \n"); + pom.append(" custom-repo\n"); + pom.append(" ").append(escapeXml(repoUrl)).append("\n"); + pom.append(" \n"); + pom.append(" true\n"); + pom.append(" \n"); + pom.append(" \n"); + pom.append(" true\n"); + pom.append(" \n"); + pom.append(" \n"); + pom.append(" \n\n"); + } + + // Build with skills-jar-plugin + pom.append(" \n"); + pom.append(" \n"); + pom.append(" \n"); + pom.append(" ").append(PLUGIN_GROUP_ID).append("\n"); + pom.append(" ").append(PLUGIN_ARTIFACT_ID).append("\n"); + pom.append(" ").append(PLUGIN_VERSION).append("\n"); + pom.append(" \n"); + pom.append(" \n"); + pom.append(" \n"); + pom.append("\n"); + + Files.writeString(projectDir.resolve("pom.xml"), pom.toString()); + } + + /** + * Generates a Maven settings.xml with server credentials for the custom repository. + */ + private void generateSettings(Path projectDir, String user, String password) throws IOException { + StringBuilder settings = new StringBuilder(); + settings.append("\n"); + settings.append("\n"); + settings.append(" \n"); + settings.append(" \n"); + settings.append(" custom-repo\n"); + settings.append(" ").append(escapeXml(user)).append("\n"); + settings.append(" ").append(escapeXml(password)).append("\n"); + settings.append(" \n"); + settings.append(" \n"); + settings.append("\n"); + + Files.writeString(projectDir.resolve("settings.xml"), settings.toString()); + } + + /** + * Runs the Maven skills-jar:install goal in the temporary project directory. + */ + private int runMaven(String mvnCommand, Path projectDir, boolean hasSettings) + throws IOException, InterruptedException { + List command = new ArrayList<>(); + command.add(mvnCommand); + command.add("-B"); // batch mode + + if (hasSettings) { + command.add("-s"); + command.add(projectDir.resolve("settings.xml").toString()); + } + + command.add(PLUGIN_GROUP_ID + ":" + PLUGIN_ARTIFACT_ID + ":" + + PLUGIN_VERSION + ":install"); + + ProcessBuilder pb = new ProcessBuilder(command); + pb.directory(projectDir.toFile()); + pb.redirectErrorStream(true); + + Process process = pb.start(); + + // Stream Maven output to stdout + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + System.out.println(line); + } + } + + return process.waitFor(); + } + + /** + * Recursively copies a directory tree from source to target. + */ + private void copyDirectory(Path source, Path target) throws IOException { + Files.walkFileTree(source, new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) + throws IOException { + Path targetPath = target.resolve(source.relativize(dir)); + Files.createDirectories(targetPath); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + Path targetPath = target.resolve(source.relativize(file)); + Files.copy(file, targetPath, StandardCopyOption.REPLACE_EXISTING); + return FileVisitResult.CONTINUE; + } + }); + } + + /** + * Recursively deletes a directory tree. + */ + private void deleteDirectory(Path dir) { + if (!Files.exists(dir)) { + return; + } + try (Stream walk = Files.walk(dir)) { + walk.sorted(Comparator.reverseOrder()) + .forEach(path -> { + try { + Files.delete(path); + } catch (IOException e) { + // best-effort cleanup + } + }); + } catch (IOException e) { + // best-effort cleanup + } + } + + /** + * Checks if a directory is empty. + */ + private boolean isDirectoryEmpty(Path dir) throws IOException { + try (Stream entries = Files.list(dir)) { + return entries.findFirst().isEmpty(); + } + } + + /** + * Escapes special XML characters in a string. + */ + private static String escapeXml(String text) { + return text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } +} From 03401bfbce29221738bc10981ceea377b0f31231 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Apr 2026 14:34:05 +0000 Subject: [PATCH 02/14] Add .gitignore for Maven build output https://claude.ai/code/session_019NWGYeNmxc8gyUBF9Y1cqF --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc2fca3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +target/ +*.class +*.jar +*.log +.idea/ +*.iml +.settings/ +.project +.classpath From 3c93ab26273b28e88455f3ddf567ca5f31600060 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Apr 2026 15:03:07 +0000 Subject: [PATCH 03/14] Add integration test for teavm-lambda skill installation - Add JUnit 5 and maven-failsafe-plugin for integration tests - Create InstallSkillIT that tests full CLI flow against ca.weblite:teavm-lambda-parent:0.1.2 - Test sets up a fixture skills JAR in @BeforeAll, installs it to the local Maven repo, then verifies the CLI resolves and unpacks it - Tests cover explicit version and default (RELEASE) version - Use skills-jar-plugin 0.1.1 (latest on Maven Central) - Use pom in generated dependency for POM-packaged artifacts https://claude.ai/code/session_019NWGYeNmxc8gyUBF9Y1cqF --- pom.xml | 35 ++++ .../installskill/InstallSkillCommand.java | 3 +- .../weblite/installskill/InstallSkillIT.java | 194 ++++++++++++++++++ 3 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 src/test/java/ca/weblite/installskill/InstallSkillIT.java diff --git a/pom.xml b/pom.xml index 61be7df..e4f4d7d 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,7 @@ UTF-8 11 4.7.5 + 5.10.2 @@ -24,6 +25,20 @@ picocli ${picocli.version} + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + @@ -43,6 +58,26 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.2.5 + + + + integration-test + verify + + + + + org.apache.maven.plugins maven-shade-plugin diff --git a/src/main/java/ca/weblite/installskill/InstallSkillCommand.java b/src/main/java/ca/weblite/installskill/InstallSkillCommand.java index 1f1f149..5f3bb79 100644 --- a/src/main/java/ca/weblite/installskill/InstallSkillCommand.java +++ b/src/main/java/ca/weblite/installskill/InstallSkillCommand.java @@ -38,7 +38,7 @@ public class InstallSkillCommand implements Callable { private static final String PLUGIN_GROUP_ID = "ca.weblite"; private static final String PLUGIN_ARTIFACT_ID = "skills-jar-plugin"; - private static final String PLUGIN_VERSION = "0.1.2"; + private static final String PLUGIN_VERSION = "0.1.1"; private static final String DEFAULT_VERSION = "RELEASE"; @Parameters(index = "0", @@ -221,6 +221,7 @@ private void generatePom(Path projectDir, String groupId, String artifactId, pom.append(" ").append(escapeXml(groupId)).append("\n"); pom.append(" ").append(escapeXml(artifactId)).append("\n"); pom.append(" ").append(escapeXml(version)).append("\n"); + pom.append(" pom\n"); pom.append(" \n"); pom.append(" \n\n"); diff --git a/src/test/java/ca/weblite/installskill/InstallSkillIT.java b/src/test/java/ca/weblite/installskill/InstallSkillIT.java new file mode 100644 index 0000000..ca8533d --- /dev/null +++ b/src/test/java/ca/weblite/installskill/InstallSkillIT.java @@ -0,0 +1,194 @@ +package ca.weblite.installskill; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration test that installs skills for ca.weblite:teavm-lambda-parent. + * + *

The teavm-lambda project publishes skills via the skills-jar-maven-plugin. + * This test creates a fixture skills JAR matching the expected format and installs + * it to the local Maven repository, then verifies the install-skill CLI can + * resolve, unpack, and copy the skill to the target directory.

+ * + *

Requires Maven on PATH.

+ */ +class InstallSkillIT { + + private static final String TEAVM_LAMBDA_GROUP = "ca.weblite"; + private static final String TEAVM_LAMBDA_ARTIFACT = "teavm-lambda-parent"; + private static final String TEAVM_LAMBDA_VERSION = "0.1.2"; + + private static final String SKILL_MD_CONTENT = + "---\n" + + "name: teavm-lambda\n" + + "description: Skill for working with teavm-lambda projects. Provides guidance on TeaVM-based serverless functions.\n" + + "---\n" + + "\n" + + "# TeaVM Lambda Skill\n" + + "\n" + + "This skill provides guidance for working with the teavm-lambda framework.\n"; + + private static Path fixtureJar; + private Path skillsDir; + + @BeforeAll + static void installFixture() throws Exception { + // Create a skills JAR matching the format produced by skills-jar-plugin + fixtureJar = Files.createTempFile("teavm-lambda-skills-", ".jar"); + try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(fixtureJar))) { + // skill/SKILL.md — the skills-jar plugin stores content under "skill/" prefix + jos.putNextEntry(new JarEntry("skill/SKILL.md")); + jos.write(SKILL_MD_CONTENT.getBytes()); + jos.closeEntry(); + } + + // Install the fixture JAR into the local Maven repository with the + // classifier "skills", matching what the skills-jar-plugin produces + ProcessBuilder pb = new ProcessBuilder( + "mvn", "-B", + "install:install-file", + "-Dfile=" + fixtureJar, + "-DgroupId=" + TEAVM_LAMBDA_GROUP, + "-DartifactId=" + TEAVM_LAMBDA_ARTIFACT, + "-Dversion=" + TEAVM_LAMBDA_VERSION, + "-Dclassifier=skills", + "-Dpackaging=jar", + "-DgeneratePom=false" + ); + pb.redirectErrorStream(true); + Process process = pb.start(); + + StringBuilder output = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append('\n'); + } + } + + int exitCode = process.waitFor(); + assertEquals(0, exitCode, + "Failed to install fixture skills JAR to local repo:\n" + output); + } + + @AfterAll + static void cleanFixture() throws IOException { + if (fixtureJar != null) { + Files.deleteIfExists(fixtureJar); + } + } + + @BeforeEach + void setUp() throws IOException { + skillsDir = Files.createTempDirectory("install-skill-it-"); + } + + @AfterEach + void tearDown() throws IOException { + if (skillsDir != null && Files.exists(skillsDir)) { + Files.walkFileTree(skillsDir, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) + throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } + } + + @Test + void installTeavmLambdaSkill() throws IOException { + int exitCode = new CommandLine(new InstallSkillCommand()).execute( + TEAVM_LAMBDA_GROUP + ":" + TEAVM_LAMBDA_ARTIFACT + ":" + TEAVM_LAMBDA_VERSION, + "-d", skillsDir.toString() + ); + + assertEquals(0, exitCode, "install-skill should exit successfully"); + + // Verify the skill directory was created (named after the artifactId) + Path skillDir = skillsDir.resolve(TEAVM_LAMBDA_ARTIFACT); + assertTrue(Files.isDirectory(skillDir), + "Skill directory should exist: " + skillDir); + + // Verify SKILL.md exists and has content + Path skillMd = skillDir.resolve("SKILL.md"); + assertTrue(Files.isRegularFile(skillMd), + "SKILL.md should exist in installed skill directory"); + + String skillContent = Files.readString(skillMd); + assertFalse(skillContent.isBlank(), "SKILL.md should not be empty"); + assertTrue(skillContent.startsWith("---"), + "SKILL.md should start with YAML frontmatter delimiter"); + assertTrue(skillContent.contains("name:"), + "SKILL.md frontmatter should contain a name field"); + assertTrue(skillContent.contains("description:"), + "SKILL.md frontmatter should contain a description field"); + assertTrue(skillContent.contains("teavm-lambda"), + "SKILL.md should reference teavm-lambda"); + + // Verify the manifest was created + Path manifest = skillsDir.resolve(".skill-manifest.json"); + assertTrue(Files.isRegularFile(manifest), + ".skill-manifest.json should exist"); + + String manifestContent = Files.readString(manifest); + assertTrue(manifestContent.contains( + TEAVM_LAMBDA_GROUP + ":" + TEAVM_LAMBDA_ARTIFACT + ":" + TEAVM_LAMBDA_VERSION), + "Manifest should contain the GAV coordinates"); + + // Verify at least the SKILL.md was installed + long fileCount; + try (Stream files = Files.walk(skillDir)) { + fileCount = files.filter(Files::isRegularFile).count(); + } + assertTrue(fileCount >= 1, + "Installed skill should contain at least 1 file, found: " + fileCount); + } + + @Test + void installTeavmLambdaSkillWithDefaultVersion() throws IOException { + // Test using RELEASE (default version) — should resolve the latest available version + int exitCode = new CommandLine(new InstallSkillCommand()).execute( + TEAVM_LAMBDA_GROUP + ":" + TEAVM_LAMBDA_ARTIFACT, + "-d", skillsDir.toString() + ); + + assertEquals(0, exitCode, "install-skill with default version should exit successfully"); + + Path skillDir = skillsDir.resolve(TEAVM_LAMBDA_ARTIFACT); + assertTrue(Files.isDirectory(skillDir), + "Skill directory should exist when using default version"); + + Path skillMd = skillDir.resolve("SKILL.md"); + assertTrue(Files.isRegularFile(skillMd), + "SKILL.md should exist when using default version"); + } +} From 819222338211130b73709c4c522b36331564ce16 Mon Sep 17 00:00:00 2001 From: Steve Hannah Date: Sat, 11 Apr 2026 08:24:40 -0700 Subject: [PATCH 04/14] Configure jDeploy for distribution via GitHub Releases and npm Add package.json with jDeploy config, GUI fallback for desktop launches, GitHub Actions workflows for both GitHub Releases and npm publishing, and .gitignore entries for jDeploy build artifacts. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/jdeploy-npm.yml | 39 +++++++++++ .github/workflows/jdeploy.yml | 65 +++++++++++++++++++ .gitignore | 3 + package.json | 32 +++++++++ .../installskill/InstallSkillCommand.java | 13 ++++ 5 files changed, 152 insertions(+) create mode 100644 .github/workflows/jdeploy-npm.yml create mode 100644 .github/workflows/jdeploy.yml create mode 100644 package.json diff --git a/.github/workflows/jdeploy-npm.yml b/.github/workflows/jdeploy-npm.yml new file mode 100644 index 0000000..007117d --- /dev/null +++ b/.github/workflows/jdeploy-npm.yml @@ -0,0 +1,39 @@ +name: jDeploy npm Publish + +on: + push: + branches: + - '*-snapshot' + tags: + - 'v*' + +concurrency: + group: jdeploy-npm-${{ github.repository }} + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + registry-url: 'https://registry.npmjs.org' + - name: Build with Maven + run: mvn clean package -DskipTests + - name: Install jDeploy + run: npm install -g jdeploy + - name: Package with jDeploy + run: jdeploy package + - name: Publish to npm + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/jdeploy.yml b/.github/workflows/jdeploy.yml new file mode 100644 index 0000000..03a8284 --- /dev/null +++ b/.github/workflows/jdeploy.yml @@ -0,0 +1,65 @@ +name: jDeploy CI + +on: + push: + branches: + - '*-snapshot' + tags: + - 'v*' + +concurrency: + group: jdeploy-release-${{ github.repository }} + cancel-in-progress: false + +jobs: + build: + permissions: + contents: write + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + - name: Build with Maven + run: mvn clean package -DskipTests + - name: Build App Installer Bundles + uses: shannah/jdeploy@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Upload Build Artifacts for DMG Action + if: ${{ vars.JDEPLOY_CREATE_DMG == 'true' }} + uses: actions/upload-artifact@v4 + with: + name: build-target + path: ./target + + create_and_upload_dmg: + if: ${{ vars.JDEPLOY_CREATE_DMG == 'true' }} + name: Create and upload DMG + permissions: + contents: write + runs-on: macos-latest + needs: build + steps: + - name: Set up Git + run: | + git config --global user.email "${{ github.actor }}@users.noreply.github.com" + git config --global user.name "${{ github.actor }}" + - uses: actions/checkout@v3 + - name: Download Build Artifacts + uses: actions/download-artifact@v4 + with: + name: build-target + path: ./target + - name: Create DMG and Upload to Release + uses: shannah/jdeploy-action-dmg@main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + developer_id: ${{ secrets.MAC_DEVELOPER_ID }} + developer_certificate_p12_base64: ${{ secrets.MAC_DEVELOPER_CERTIFICATE_P12_BASE64 }} + developer_certificate_password: ${{ secrets.MAC_DEVELOPER_CERTIFICATE_PASSWORD }} + notarization_password: ${{ secrets.MAC_NOTARIZATION_PASSWORD }} diff --git a/.gitignore b/.gitignore index cc2fca3..6b8fa27 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ target/ .settings/ .project .classpath +jdeploy-bundle/ +jdeploy/ +node_modules/ diff --git a/package.json b/package.json new file mode 100644 index 0000000..60c0dc7 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "install-skill-cli", + "version": "1.0.0", + "description": "CLI tool for installing skills deployed with the skills-jar-maven-plugin", + "bin": { + "install-skill": "jdeploy-bundle/jdeploy.js" + }, + "preferGlobal": true, + "jdeploy": { + "jdk": false, + "javaVersion": "11", + "javafx": false, + "jar": "target/install-skill-cli-1.0.0-SNAPSHOT.jar", + "title": "Install Skill CLI", + "commands": { + "install-skill": { + "description": "Install skills deployed with the skills-jar-maven-plugin" + } + } + }, + "dependencies": { + "command-exists-promise": "^2.0.2", + "node-fetch": "2.6.7", + "tar": "^4.4.8", + "yauzl": "^2.10.0", + "shelljs": "^0.8.4" + }, + "files": [ + "jdeploy-bundle" + ], + "license": "ISC" +} diff --git a/src/main/java/ca/weblite/installskill/InstallSkillCommand.java b/src/main/java/ca/weblite/installskill/InstallSkillCommand.java index 5f3bb79..d5c745f 100644 --- a/src/main/java/ca/weblite/installskill/InstallSkillCommand.java +++ b/src/main/java/ca/weblite/installskill/InstallSkillCommand.java @@ -58,6 +58,19 @@ public class InstallSkillCommand implements Callable { private String skillsDir; public static void main(String[] args) { + String mode = System.getProperty("jdeploy.mode", "gui"); + if ("gui".equals(mode)) { + javax.swing.SwingUtilities.invokeLater(() -> { + javax.swing.JOptionPane.showMessageDialog( + null, + "Install Skill CLI v1.0.0\n\nThis is a command-line tool.\nRun 'install-skill' in a terminal for usage.", + "About Install Skill CLI", + javax.swing.JOptionPane.INFORMATION_MESSAGE + ); + System.exit(0); + }); + return; + } int exitCode = new CommandLine(new InstallSkillCommand()).execute(args); System.exit(exitCode); } From 04f0c4e234ce74a7b36cbe94550983f4dcd367d3 Mon Sep 17 00:00:00 2001 From: Steve Hannah Date: Sat, 11 Apr 2026 08:26:11 -0700 Subject: [PATCH 05/14] Add CI workflow to build and test on PRs to main Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bb93c68 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: CI + +on: + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + - name: Build and test + run: mvn clean verify From 26f8381bf2004fbde26b9f9593987110e2473f80 Mon Sep 17 00:00:00 2001 From: Steve Hannah Date: Sat, 11 Apr 2026 08:36:32 -0700 Subject: [PATCH 06/14] Default jdeploy.mode to null so GUI fallback only triggers when explicitly set Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main/java/ca/weblite/installskill/InstallSkillCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ca/weblite/installskill/InstallSkillCommand.java b/src/main/java/ca/weblite/installskill/InstallSkillCommand.java index d5c745f..af513c1 100644 --- a/src/main/java/ca/weblite/installskill/InstallSkillCommand.java +++ b/src/main/java/ca/weblite/installskill/InstallSkillCommand.java @@ -58,7 +58,7 @@ public class InstallSkillCommand implements Callable { private String skillsDir; public static void main(String[] args) { - String mode = System.getProperty("jdeploy.mode", "gui"); + String mode = System.getProperty("jdeploy.mode", null); if ("gui".equals(mode)) { javax.swing.SwingUtilities.invokeLater(() -> { javax.swing.JOptionPane.showMessageDialog( From 8f1b40b59c7a497d93e77be968540ec7a4f581e3 Mon Sep 17 00:00:00 2001 From: Steve Hannah Date: Sat, 11 Apr 2026 08:37:42 -0700 Subject: [PATCH 07/14] Bump version to 1.0.1 Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 60c0dc7..01054a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "install-skill-cli", - "version": "1.0.0", + "version": "1.0.1", "description": "CLI tool for installing skills deployed with the skills-jar-maven-plugin", "bin": { "install-skill": "jdeploy-bundle/jdeploy.js" From e0f8e43d23c43704e1a917b2170ef979194aef96 Mon Sep 17 00:00:00 2001 From: Steve Hannah Date: Sat, 11 Apr 2026 08:40:57 -0700 Subject: [PATCH 08/14] Bump version to 1.0.2 Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 01054a8..23a73a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "install-skill-cli", - "version": "1.0.1", + "version": "1.0.2", "description": "CLI tool for installing skills deployed with the skills-jar-maven-plugin", "bin": { "install-skill": "jdeploy-bundle/jdeploy.js" From 5511e59cbff9268a6b6357d6f0924d85782978a2 Mon Sep 17 00:00:00 2001 From: Steve Hannah Date: Sat, 11 Apr 2026 08:44:23 -0700 Subject: [PATCH 09/14] Bundle Maven by auto-downloading and caching when not on PATH Downloads Apache Maven 3.9.9 to ~/.install-skill-cli/ on first use if mvn is not found on the system PATH, removing the requirement for users to have Maven pre-installed. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../installskill/InstallSkillCommand.java | 105 +++++++++++++++--- 1 file changed, 88 insertions(+), 17 deletions(-) diff --git a/src/main/java/ca/weblite/installskill/InstallSkillCommand.java b/src/main/java/ca/weblite/installskill/InstallSkillCommand.java index af513c1..96c1a52 100644 --- a/src/main/java/ca/weblite/installskill/InstallSkillCommand.java +++ b/src/main/java/ca/weblite/installskill/InstallSkillCommand.java @@ -8,8 +8,11 @@ import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.UncheckedIOException; +import java.net.HttpURLConnection; +import java.net.URL; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -22,6 +25,8 @@ import java.util.List; import java.util.concurrent.Callable; import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; /** * CLI tool that installs skills deployed with the skills-jar-maven-plugin. @@ -40,6 +45,10 @@ public class InstallSkillCommand implements Callable { private static final String PLUGIN_ARTIFACT_ID = "skills-jar-plugin"; private static final String PLUGIN_VERSION = "0.1.1"; private static final String DEFAULT_VERSION = "RELEASE"; + private static final String MAVEN_VERSION = "3.9.9"; + private static final String MAVEN_DOWNLOAD_URL = + "https://dlcdn.apache.org/maven/maven-3/" + MAVEN_VERSION + + "/binaries/apache-maven-" + MAVEN_VERSION + "-bin.zip"; @Parameters(index = "0", description = "Skill coordinates: groupId:artifactId[:version]. Version defaults to RELEASE (latest release).", @@ -105,10 +114,10 @@ public Integer call() throws Exception { repoPass = repoParts[2]; } - // 3. Verify Maven is available + // 3. Ensure Maven is available (downloads if not on PATH) String mvnCommand = findMavenCommand(); if (mvnCommand == null) { - System.err.println("Error: Maven (mvn) is not available on the PATH. Please install Maven first."); + System.err.println("Error: Failed to locate or download Maven."); return 1; } @@ -181,34 +190,96 @@ static String[] parseRepository(String repo) { } /** - * Finds the Maven command (mvn) on the system PATH. + * Finds Maven on the system PATH, or downloads and caches it if not found. */ private String findMavenCommand() { - // Try mvn directly + // Try system mvn + if (tryCommand("mvn")) return "mvn"; + if (tryCommand("mvn.cmd")) return "mvn.cmd"; + + // Try cached Maven + Path cachedMvn = getCachedMavenBin("mvn"); + if (Files.isExecutable(cachedMvn)) { + return cachedMvn.toString(); + } + // Windows + Path cachedMvnCmd = getCachedMavenBin("mvn.cmd"); + if (Files.exists(cachedMvnCmd)) { + return cachedMvnCmd.toString(); + } + + // Download and cache Maven try { - Process p = new ProcessBuilder("mvn", "--version") - .redirectErrorStream(true) - .start(); - p.getInputStream().readAllBytes(); - if (p.waitFor() == 0) { - return "mvn"; + downloadMaven(); + if (Files.exists(cachedMvn)) { + cachedMvn.toFile().setExecutable(true); + return cachedMvn.toString(); } - } catch (Exception ignored) { + if (Files.exists(cachedMvnCmd)) { + return cachedMvnCmd.toString(); + } + } catch (IOException e) { + System.err.println("Error: Failed to download Maven: " + e.getMessage()); } - // Try mvn.cmd for Windows + return null; + } + + private boolean tryCommand(String command) { try { - Process p = new ProcessBuilder("mvn.cmd", "--version") + Process p = new ProcessBuilder(command, "--version") .redirectErrorStream(true) .start(); p.getInputStream().readAllBytes(); - if (p.waitFor() == 0) { - return "mvn.cmd"; - } + return p.waitFor() == 0; } catch (Exception ignored) { + return false; } + } - return null; + private Path getCachedMavenBin(String executable) { + return Paths.get(System.getProperty("user.home"), ".install-skill-cli", + "apache-maven-" + MAVEN_VERSION, "bin", executable); + } + + private void downloadMaven() throws IOException { + Path cacheDir = Paths.get(System.getProperty("user.home"), ".install-skill-cli"); + Files.createDirectories(cacheDir); + + System.out.println("Downloading Apache Maven " + MAVEN_VERSION + "..."); + URL url = new URL(MAVEN_DOWNLOAD_URL); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setInstanceFollowRedirects(true); + + try (InputStream in = conn.getInputStream(); + ZipInputStream zis = new ZipInputStream(in)) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + Path target = cacheDir.resolve(entry.getName()).normalize(); + // Zip slip protection + if (!target.startsWith(cacheDir)) { + throw new IOException("Bad zip entry: " + entry.getName()); + } + if (entry.isDirectory()) { + Files.createDirectories(target); + } else { + Files.createDirectories(target.getParent()); + Files.copy(zis, target, StandardCopyOption.REPLACE_EXISTING); + } + } + } finally { + conn.disconnect(); + } + + // Set execute permissions on bin scripts + Path binDir = cacheDir.resolve("apache-maven-" + MAVEN_VERSION).resolve("bin"); + if (Files.isDirectory(binDir)) { + try (Stream files = Files.list(binDir)) { + files.forEach(f -> f.toFile().setExecutable(true)); + } + } + + System.out.println("Maven " + MAVEN_VERSION + " installed to " + cacheDir); } /** From 077afd9b384749806aa1ad2438a1ea6d820c5a10 Mon Sep 17 00:00:00 2001 From: Steve Hannah Date: Sat, 11 Apr 2026 08:45:05 -0700 Subject: [PATCH 10/14] Bump version to 1.0.3 Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 23a73a3..dc1660b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "install-skill-cli", - "version": "1.0.2", + "version": "1.0.3", "description": "CLI tool for installing skills deployed with the skills-jar-maven-plugin", "bin": { "install-skill": "jdeploy-bundle/jdeploy.js" From 13c64040767e0a6d81de552f894b15bc2aac8aeb Mon Sep 17 00:00:00 2001 From: Steve Hannah Date: Sat, 11 Apr 2026 08:50:55 -0700 Subject: [PATCH 11/14] Use embedded Maven instead of external mvn command Replace the external Maven process execution and download logic with the Maven Embedder API. Maven now runs in-process as a bundled dependency, eliminating the need for mvn on the system PATH. Bump version to 1.0.4. Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- pom.xml | 26 +++ .../installskill/InstallSkillCommand.java | 163 ++---------------- 3 files changed, 46 insertions(+), 145 deletions(-) diff --git a/package.json b/package.json index dc1660b..f0502e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "install-skill-cli", - "version": "1.0.3", + "version": "1.0.4", "description": "CLI tool for installing skills deployed with the skills-jar-maven-plugin", "bin": { "install-skill": "jdeploy-bundle/jdeploy.js" diff --git a/pom.xml b/pom.xml index e4f4d7d..c20c408 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,28 @@ ${picocli.version} + + + org.apache.maven + maven-embedder + 3.9.9 + + + org.apache.maven + maven-compat + 3.9.9 + + + org.apache.maven.resolver + maven-resolver-connector-basic + 1.9.22 + + + org.apache.maven.resolver + maven-resolver-transport-http + 1.9.22 + + org.junit.jupiter @@ -93,6 +115,10 @@ ca.weblite.installskill.InstallSkillCommand + + + META-INF/sisu/javax.inject.Named + false diff --git a/src/main/java/ca/weblite/installskill/InstallSkillCommand.java b/src/main/java/ca/weblite/installskill/InstallSkillCommand.java index 96c1a52..18d071a 100644 --- a/src/main/java/ca/weblite/installskill/InstallSkillCommand.java +++ b/src/main/java/ca/weblite/installskill/InstallSkillCommand.java @@ -1,18 +1,14 @@ package ca.weblite.installskill; +import org.apache.maven.cli.MavenCli; import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; -import java.io.BufferedReader; import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.io.UncheckedIOException; -import java.net.HttpURLConnection; -import java.net.URL; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -25,8 +21,6 @@ import java.util.List; import java.util.concurrent.Callable; import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; /** * CLI tool that installs skills deployed with the skills-jar-maven-plugin. @@ -45,10 +39,6 @@ public class InstallSkillCommand implements Callable { private static final String PLUGIN_ARTIFACT_ID = "skills-jar-plugin"; private static final String PLUGIN_VERSION = "0.1.1"; private static final String DEFAULT_VERSION = "RELEASE"; - private static final String MAVEN_VERSION = "3.9.9"; - private static final String MAVEN_DOWNLOAD_URL = - "https://dlcdn.apache.org/maven/maven-3/" + MAVEN_VERSION - + "/binaries/apache-maven-" + MAVEN_VERSION + "-bin.zip"; @Parameters(index = "0", description = "Skill coordinates: groupId:artifactId[:version]. Version defaults to RELEASE (latest release).", @@ -114,31 +104,24 @@ public Integer call() throws Exception { repoPass = repoParts[2]; } - // 3. Ensure Maven is available (downloads if not on PATH) - String mvnCommand = findMavenCommand(); - if (mvnCommand == null) { - System.err.println("Error: Failed to locate or download Maven."); - return 1; - } - - // 4. Create temp directory + // 3. Create temp directory Path tempDir = Files.createTempDirectory("install-skill-"); System.out.println("Setting up temporary Maven project..."); try { - // 5. Generate pom.xml in the temp project + // 4. Generate pom.xml in the temp project generatePom(tempDir, groupId, artifactId, version, repoUrl); - // 6. Generate settings.xml if credentials are provided + // 5. Generate settings.xml if credentials are provided boolean hasSettings = false; if (repoUser != null) { generateSettings(tempDir, repoUser, repoPass); hasSettings = true; } - // 7. Run Maven to install skills + // 6. Run embedded Maven to install skills System.out.println("Resolving skill " + groupId + ":" + artifactId + ":" + version + "..."); - int exitCode = runMaven(mvnCommand, tempDir, hasSettings); + int exitCode = runMaven(tempDir, hasSettings); if (exitCode != 0) { System.err.println("Error: Maven execution failed (exit code " + exitCode + ")."); return exitCode; @@ -190,96 +173,23 @@ static String[] parseRepository(String repo) { } /** - * Finds Maven on the system PATH, or downloads and caches it if not found. + * Runs the Maven skills-jar:install goal using the embedded Maven runtime. */ - private String findMavenCommand() { - // Try system mvn - if (tryCommand("mvn")) return "mvn"; - if (tryCommand("mvn.cmd")) return "mvn.cmd"; - - // Try cached Maven - Path cachedMvn = getCachedMavenBin("mvn"); - if (Files.isExecutable(cachedMvn)) { - return cachedMvn.toString(); - } - // Windows - Path cachedMvnCmd = getCachedMavenBin("mvn.cmd"); - if (Files.exists(cachedMvnCmd)) { - return cachedMvnCmd.toString(); - } - - // Download and cache Maven - try { - downloadMaven(); - if (Files.exists(cachedMvn)) { - cachedMvn.toFile().setExecutable(true); - return cachedMvn.toString(); - } - if (Files.exists(cachedMvnCmd)) { - return cachedMvnCmd.toString(); - } - } catch (IOException e) { - System.err.println("Error: Failed to download Maven: " + e.getMessage()); - } + private int runMaven(Path projectDir, boolean hasSettings) { + System.setProperty("maven.multiModuleProjectDirectory", projectDir.toString()); - return null; - } - - private boolean tryCommand(String command) { - try { - Process p = new ProcessBuilder(command, "--version") - .redirectErrorStream(true) - .start(); - p.getInputStream().readAllBytes(); - return p.waitFor() == 0; - } catch (Exception ignored) { - return false; - } - } - - private Path getCachedMavenBin(String executable) { - return Paths.get(System.getProperty("user.home"), ".install-skill-cli", - "apache-maven-" + MAVEN_VERSION, "bin", executable); - } - - private void downloadMaven() throws IOException { - Path cacheDir = Paths.get(System.getProperty("user.home"), ".install-skill-cli"); - Files.createDirectories(cacheDir); - - System.out.println("Downloading Apache Maven " + MAVEN_VERSION + "..."); - URL url = new URL(MAVEN_DOWNLOAD_URL); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setInstanceFollowRedirects(true); - - try (InputStream in = conn.getInputStream(); - ZipInputStream zis = new ZipInputStream(in)) { - ZipEntry entry; - while ((entry = zis.getNextEntry()) != null) { - Path target = cacheDir.resolve(entry.getName()).normalize(); - // Zip slip protection - if (!target.startsWith(cacheDir)) { - throw new IOException("Bad zip entry: " + entry.getName()); - } - if (entry.isDirectory()) { - Files.createDirectories(target); - } else { - Files.createDirectories(target.getParent()); - Files.copy(zis, target, StandardCopyOption.REPLACE_EXISTING); - } - } - } finally { - conn.disconnect(); - } - - // Set execute permissions on bin scripts - Path binDir = cacheDir.resolve("apache-maven-" + MAVEN_VERSION).resolve("bin"); - if (Files.isDirectory(binDir)) { - try (Stream files = Files.list(binDir)) { - files.forEach(f -> f.toFile().setExecutable(true)); - } + List args = new ArrayList<>(); + args.add("-B"); + if (hasSettings) { + args.add("-s"); + args.add(projectDir.resolve("settings.xml").toString()); } + args.add(PLUGIN_GROUP_ID + ":" + PLUGIN_ARTIFACT_ID + ":" + + PLUGIN_VERSION + ":install"); - System.out.println("Maven " + MAVEN_VERSION + " installed to " + cacheDir); + MavenCli cli = new MavenCli(); + return cli.doMain(args.toArray(new String[0]), + projectDir.toString(), System.out, System.err); } /** @@ -376,41 +286,6 @@ private void generateSettings(Path projectDir, String user, String password) thr Files.writeString(projectDir.resolve("settings.xml"), settings.toString()); } - /** - * Runs the Maven skills-jar:install goal in the temporary project directory. - */ - private int runMaven(String mvnCommand, Path projectDir, boolean hasSettings) - throws IOException, InterruptedException { - List command = new ArrayList<>(); - command.add(mvnCommand); - command.add("-B"); // batch mode - - if (hasSettings) { - command.add("-s"); - command.add(projectDir.resolve("settings.xml").toString()); - } - - command.add(PLUGIN_GROUP_ID + ":" + PLUGIN_ARTIFACT_ID + ":" - + PLUGIN_VERSION + ":install"); - - ProcessBuilder pb = new ProcessBuilder(command); - pb.directory(projectDir.toFile()); - pb.redirectErrorStream(true); - - Process process = pb.start(); - - // Stream Maven output to stdout - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) { - System.out.println(line); - } - } - - return process.waitFor(); - } - /** * Recursively copies a directory tree from source to target. */ From d62f954b2ff845505dcab4a93043bfe0b6134f14 Mon Sep 17 00:00:00 2001 From: Steve Hannah Date: Sat, 11 Apr 2026 08:52:39 -0700 Subject: [PATCH 12/14] Fix embedded Maven: add ComponentsXml transformer and wagon-http The shaded JAR was missing merged Plexus components.xml files and the Wagon HTTP transport, causing RepositorySystem binding failures. Co-Authored-By: Claude Opus 4.6 (1M context) --- pom.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pom.xml b/pom.xml index c20c408..7cf0eeb 100644 --- a/pom.xml +++ b/pom.xml @@ -47,6 +47,16 @@ maven-resolver-transport-http 1.9.22 + + org.apache.maven.wagon + wagon-http + 3.5.3 + + + org.slf4j + slf4j-simple + 1.7.36 + @@ -116,6 +126,7 @@ ca.weblite.installskill.InstallSkillCommand + META-INF/sisu/javax.inject.Named From c8168e57c6526e4773954fc77bf66ad83f58d2e8 Mon Sep 17 00:00:00 2001 From: Steve Hannah Date: Sat, 11 Apr 2026 08:53:07 -0700 Subject: [PATCH 13/14] Bump version to 1.0.5 Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f0502e3..51e41df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "install-skill-cli", - "version": "1.0.4", + "version": "1.0.5", "description": "CLI tool for installing skills deployed with the skills-jar-maven-plugin", "bin": { "install-skill": "jdeploy-bundle/jdeploy.js" From 0a63dc4feef7660d82b4439aca3f57b435184733 Mon Sep 17 00:00:00 2001 From: Steve Hannah Date: Sat, 11 Apr 2026 09:08:36 -0700 Subject: [PATCH 14/14] Add --global flag for installing skills to ~/.claude/skills Default installs locally to ./.claude/skills. Use -g/--global to install to ~/.claude/skills. The -d option overrides both. Bump version to 1.0.6. Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- .../weblite/installskill/InstallSkillCommand.java | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 51e41df..b19eaa6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "install-skill-cli", - "version": "1.0.5", + "version": "1.0.6", "description": "CLI tool for installing skills deployed with the skills-jar-maven-plugin", "bin": { "install-skill": "jdeploy-bundle/jdeploy.js" diff --git a/src/main/java/ca/weblite/installskill/InstallSkillCommand.java b/src/main/java/ca/weblite/installskill/InstallSkillCommand.java index 18d071a..5602584 100644 --- a/src/main/java/ca/weblite/installskill/InstallSkillCommand.java +++ b/src/main/java/ca/weblite/installskill/InstallSkillCommand.java @@ -50,10 +50,13 @@ public class InstallSkillCommand implements Callable { paramLabel = "") private String repository; + @Option(names = {"-g", "--global"}, + description = "Install globally to ~/.claude/skills (default is local: ./.claude/skills).") + private boolean global; + @Option(names = {"-d"}, - description = "Skills installation directory (default: ./claude/skills).", - paramLabel = "", - defaultValue = "./claude/skills") + description = "Skills installation directory (overrides --global).", + paramLabel = "") private String skillsDir; public static void main(String[] args) { @@ -129,7 +132,10 @@ public Integer call() throws Exception { // 8. Copy installed skills to target directory Path installedSkillsDir = tempDir.resolve(".claude").resolve("skills"); - Path targetDir = Paths.get(skillsDir).toAbsolutePath().normalize(); + String resolvedDir = skillsDir != null ? skillsDir + : global ? Paths.get(System.getProperty("user.home"), ".claude", "skills").toString() + : ".claude/skills"; + Path targetDir = Paths.get(resolvedDir).toAbsolutePath().normalize(); if (!Files.exists(installedSkillsDir) || isDirectoryEmpty(installedSkillsDir)) { System.err.println("Error: No skills were found after installation.");