From 1c32a91ba7581f1f79690e6d8a2cfd822e3f1994 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 24 Mar 2026 07:52:57 +0100 Subject: [PATCH 01/12] CAMEL-23236: Add context-aware shell banner The shell now prints a banner on startup that detects route files in the current directory and shows appropriate quick-start hints for beginners. When no routes are found, it suggests init, doc, and help commands. When routes are present, it suggests run and run --dev commands. Co-Authored-By: Claude Opus 4.6 --- .../camel/dsl/jbang/core/commands/Shell.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java index 4eb84de7e2b58..df25929c3433a 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java @@ -16,10 +16,15 @@ */ package org.apache.camel.dsl.jbang.core.commands; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.function.Supplier; +import java.util.stream.Stream; +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; import org.apache.camel.util.HomeHelper; import org.jline.builtins.ClasspathResourceUtil; import org.jline.builtins.ConfigurationPath; @@ -100,6 +105,9 @@ public String name() { String prompt = "camel> "; String rightPrompt = null; + // print context-aware banner + printBanner(terminal); + // start the shell and process input until the user quits with Ctrl-C or Ctrl-D String line; boolean run = true; @@ -125,6 +133,46 @@ public String name() { return 0; } + private void printBanner(Terminal terminal) { + CamelCatalog catalog = new DefaultCamelCatalog(); + String version = catalog.getCatalogVersion(); + terminal.writer().println("Apache Camel JBang Shell v" + version); + + // detect route files in current directory + int routeCount = countRouteFiles(); + if (routeCount == 0) { + terminal.writer().println("No routes found in current directory."); + terminal.writer().println(" Quick start: init MyRoute.yaml && run *"); + terminal.writer().println(" Templates: init --list"); + terminal.writer().println(" Docs: doc "); + terminal.writer().println(" Need help? help"); + } else { + terminal.writer().printf("Found %d route file(s) in current directory.%n", routeCount); + terminal.writer().println(" Run: run *"); + terminal.writer().println(" Watch: run * --dev"); + } + terminal.writer().println(); + terminal.writer().flush(); + } + + private static int countRouteFiles() { + int count = 0; + try (Stream files = Files.list(Paths.get("."))) { + count = (int) files.filter(Files::isRegularFile) + .filter(p -> { + String name = p.getFileName().toString(); + return name.endsWith(".yaml") && !name.endsWith(".kamelet.yaml") + && !name.equals("application.yaml") + || name.endsWith(".xml") && !name.equals("pom.xml") + || name.endsWith(".java"); + }) + .count(); + } catch (IOException e) { + // ignore + } + return count; + } + private static class ReplHighlighter extends DefaultHighlighter { @Override protected void commandStyle(LineReader reader, AttributedStringBuilder sb, boolean enable) { From df6b1abb4bb10876460a1e15b087a3fb8de40caf Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 24 Mar 2026 07:53:46 +0100 Subject: [PATCH 02/12] CAMEL-23236: Add interactive template picker to camel init When `camel init` is invoked with no arguments in an interactive terminal (TTY, non-CI), show an interactive template picker that guides the user through selecting a category, template, and filename. Non-interactive behavior is unchanged. Co-Authored-By: Claude Opus 4.6 --- .../camel/dsl/jbang/core/commands/Init.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java index 1a0f967b8bff0..c4fb6846e2997 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java @@ -22,8 +22,11 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Scanner; import java.util.Stack; import java.util.StringJoiner; @@ -95,7 +98,12 @@ public Integer doCall() throws Exception { return listTemplates(); } if (file == null) { + // try interactive picker if running in a TTY and not in CI + if (System.console() != null && System.getenv("CI") == null) { + return interactivePicker(); + } printer().printErr("Missing required parameter: "); + printer().printErr("Run 'camel init --list' to see available templates, or run interactively in a terminal."); return 1; } int code = execute(); @@ -271,6 +279,91 @@ private int listTemplates() { return 0; } + private int interactivePicker() throws Exception { + // Build template categories + Map> categories = new LinkedHashMap<>(); + categories.put("Routes", List.of( + new String[] { "yaml", "YAML DSL route", ".yaml" }, + new String[] { "java", "Java DSL route", ".java" }, + new String[] { "xml", "XML DSL route", ".xml" })); + categories.put("Kamelets", List.of( + new String[] { "kamelet-source.yaml", "Kamelet source connector", ".kamelet.yaml" }, + new String[] { "kamelet-sink.yaml", "Kamelet sink connector", ".kamelet.yaml" }, + new String[] { "kamelet-action.yaml", "Kamelet action processor", ".kamelet.yaml" })); + categories.put("Pipes and CRs", List.of( + new String[] { "init-pipe.yaml", "Pipe CR (source to sink)", ".yaml" })); + + Scanner scanner = new Scanner(System.in); + + // Step 1: Pick a category + printer().println("Select a template category:"); + List categoryNames = new ArrayList<>(categories.keySet()); + for (int i = 0; i < categoryNames.size(); i++) { + printer().printf(" %d) %s%n", i + 1, categoryNames.get(i)); + } + printer().print("Choice [1]: "); + String categoryInput = scanner.nextLine().trim(); + int categoryIdx = categoryInput.isEmpty() ? 0 : Integer.parseInt(categoryInput) - 1; + if (categoryIdx < 0 || categoryIdx >= categoryNames.size()) { + printer().printErr("Invalid choice."); + return 1; + } + + // Step 2: Pick a template + String selectedCategory = categoryNames.get(categoryIdx); + List templates = categories.get(selectedCategory); + printer().println(); + printer().println("Select a template:"); + for (int i = 0; i < templates.size(); i++) { + printer().printf(" %d) %s%n", i + 1, templates.get(i)[1]); + } + printer().print("Choice [1]: "); + String templateInput = scanner.nextLine().trim(); + int templateIdx = templateInput.isEmpty() ? 0 : Integer.parseInt(templateInput) - 1; + if (templateIdx < 0 || templateIdx >= templates.size()) { + printer().printErr("Invalid choice."); + return 1; + } + + String[] selected = templates.get(templateIdx); + String ext = selected[2]; + String defaultName = "MyRoute" + ext; + if (ext.endsWith(".kamelet.yaml")) { + if (selected[0].contains("source")) { + defaultName = "my-source.kamelet.yaml"; + } else if (selected[0].contains("sink")) { + defaultName = "my-sink.kamelet.yaml"; + } else { + defaultName = "my-action.kamelet.yaml"; + } + } else if (selected[0].contains("pipe")) { + defaultName = "my-pipe.yaml"; + pipe = true; + } + + // Step 3: Prompt for filename + printer().println(); + printer().printf("Filename [%s]: ", defaultName); + String filename = scanner.nextLine().trim(); + if (filename.isEmpty()) { + filename = defaultName; + } + + this.file = filename; + int code = execute(); + if (code == 0) { + createWorkingDirectoryIfAbsent(); + printer().println(); + printer().println("Created: " + filename); + printer().println(); + printer().println("Next steps:"); + printer().println(" Run: camel run " + filename); + printer().println(" Run (live): camel run " + filename + " --dev"); + printer().println(" Documentation: camel doc "); + } + return code; + } + private void createWorkingDirectoryIfAbsent() { Path work = CommandLineHelper.getWorkDir(); if (!Files.exists(work)) { From 79203b189771356d1c71692654de0fbb54756612 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 24 Mar 2026 07:54:41 +0100 Subject: [PATCH 03/12] CAMEL-23236: Add camel run --example for zero-to-running experience Allow running built-in examples without creating any files first: camel run --example timer-log camel run --example rest-api camel run --example cron-log Use --example-list to show available examples. Examples are bundled YAML route files extracted to a temp directory at runtime. Co-Authored-By: Claude Opus 4.6 --- .../camel/dsl/jbang/core/commands/Run.java | 63 +++++++++++++++++++ .../src/main/resources/examples/cron-log.yaml | 10 +++ .../src/main/resources/examples/rest-api.yaml | 21 +++++++ .../main/resources/examples/timer-log.yaml | 10 +++ 4 files changed, 104 insertions(+) create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log.yaml create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api.yaml create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java index 653b99ea85412..cefeb6046b1b5 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java @@ -325,6 +325,15 @@ public class Run extends CamelCommand { description = "Skip resolving plugin dependencies") boolean skipPlugins; + @Option(names = { "--example" }, + description = "Run a built-in example by name (e.g., timer-log, rest-api). Use --example --list to show available examples.", + arity = "0..1", fallbackValue = "") + String example; + + @Option(names = { "--example-list" }, + description = "List available built-in examples") + boolean exampleList; + public Run(CamelJBangMain main) { super(main); } @@ -344,6 +353,14 @@ public boolean disarrangeLogging() { @Override public Integer doCall() throws Exception { + // handle --example + if (exampleList || (example != null && example.isEmpty())) { + return listExamples(); + } + if (example != null) { + return runExample(); + } + if (!exportRun) { printConfigurationValues("Running integration with the following configuration:"); } @@ -351,6 +368,52 @@ public Integer doCall() throws Exception { return run(); } + private int listExamples() { + printer().println("Available built-in examples:"); + printer().println(); + printer().printf(" %-20s %s%n", "timer-log", "Simple timer that logs messages every second"); + printer().printf(" %-20s %s%n", "rest-api", "REST API with hello endpoints"); + printer().printf(" %-20s %s%n", "cron-log", "Scheduled task that logs every 5 seconds"); + printer().println(); + printer().println("Usage: camel run --example "); + printer().println(" camel run --example --dev"); + return 0; + } + + private int runExample() throws Exception { + String resourcePath = "examples/" + example + ".yaml"; + InputStream is = Run.class.getClassLoader().getResourceAsStream(resourcePath); + if (is == null) { + printer().printErr("Unknown example: " + example); + printer().printErr("Run 'camel run --example-list' to see available examples."); + return 1; + } + + // extract example to a temp file and run it + Path tempDir = Files.createTempDirectory("camel-example-"); + Path exampleFile = tempDir.resolve(example + ".yaml"); + try { + String content = IOHelper.loadText(is); + IOHelper.close(is); + Files.writeString(exampleFile, content); + + printer().println("Running example: " + example); + files.add(exampleFile.toString()); + if ("CamelJBang".equals(name)) { + name = example; + } + + if (!exportRun) { + printConfigurationValues("Running integration with the following configuration:"); + } + return run(); + } finally { + // clean up temp files on JVM exit + exampleFile.toFile().deleteOnExit(); + tempDir.toFile().deleteOnExit(); + } + } + public Integer runExport() throws Exception { return runExport(false); } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log.yaml new file mode 100644 index 0000000000000..c31bdb1cec3d0 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log.yaml @@ -0,0 +1,10 @@ +- route: + id: cron-log + from: + uri: timer:cron + parameters: + period: "5000" + steps: + - setBody: + simple: "Scheduled task running at ${date:now:HH:mm:ss}" + - log: "${body}" diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api.yaml new file mode 100644 index 0000000000000..2e1035cf1f0f4 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api.yaml @@ -0,0 +1,21 @@ +- rest: + path: /api + get: + - path: /hello + to: direct:hello + - path: /hello/{name} + to: direct:hello-name +- route: + id: hello + from: + uri: direct:hello + steps: + - setBody: + constant: "Hello from Camel REST API!" +- route: + id: hello-name + from: + uri: direct:hello-name + steps: + - setBody: + simple: "Hello ${header.name} from Camel REST API!" diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log.yaml new file mode 100644 index 0000000000000..cdff71b4d71f5 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log.yaml @@ -0,0 +1,10 @@ +- route: + id: timer-log + from: + uri: timer:tick + parameters: + period: "1000" + steps: + - setBody: + simple: "Hello Camel! (message #${exchangeProperty.CamelTimerCounter})" + - log: "${body}" From 82271a299da446b38f5c97ceb6aa97c65a08c8ce Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 24 Mar 2026 07:56:47 +0100 Subject: [PATCH 04/12] CAMEL-23236: Extend did-you-mean suggestions to more commands - CatalogBaseCommand: when filter returns no results, suggest similar names using SuggestSimilarHelper and hint about 'camel doc' - Run --example: suggest similar example names when unknown example given Co-Authored-By: Claude Opus 4.6 --- .../org/apache/camel/dsl/jbang/core/commands/Run.java | 10 +++++++++- .../core/commands/catalog/CatalogBaseCommand.java | 11 +++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java index cefeb6046b1b5..1429d77f2a47b 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java @@ -380,11 +380,19 @@ private int listExamples() { return 0; } + private static final List EXAMPLE_NAMES = List.of("timer-log", "rest-api", "cron-log"); + private int runExample() throws Exception { String resourcePath = "examples/" + example + ".yaml"; InputStream is = Run.class.getClassLoader().getResourceAsStream(resourcePath); if (is == null) { - printer().printErr("Unknown example: " + example); + List suggestions + = org.apache.camel.main.util.SuggestSimilarHelper.didYouMean(EXAMPLE_NAMES, example); + if (!suggestions.isEmpty()) { + printer().printErr("Unknown example: " + example + ". Did you mean? " + String.join(", ", suggestions)); + } else { + printer().printErr("Unknown example: " + example); + } printer().printErr("Run 'camel run --example-list' to see available examples."); return 1; } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java index 288d7ad3defb8..3cdc5b1cdeea1 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java @@ -35,6 +35,7 @@ import org.apache.camel.dsl.jbang.core.common.RuntimeTypeConverter; import org.apache.camel.dsl.jbang.core.common.VersionHelper; import org.apache.camel.dsl.jbang.core.model.CatalogBaseDTO; +import org.apache.camel.main.util.SuggestSimilarHelper; import org.apache.camel.tooling.maven.MavenGav; import org.apache.camel.tooling.model.ArtifactModel; import org.apache.camel.util.json.Jsoner; @@ -167,6 +168,16 @@ public Integer doCall() throws Exception { new Column().header("SINCE").dataAlign(HorizontalAlign.RIGHT).with(r -> r.since), new Column().header("DESCRIPTION").dataAlign(HorizontalAlign.LEFT).with(this::shortDescription)))); } + } else if (filterName != null) { + // suggest similar names when filter returns no results + List allNames = collectRows().stream().map(r -> r.name).collect(Collectors.toList()); + List suggestions = SuggestSimilarHelper.didYouMean(allNames, filterName); + if (!suggestions.isEmpty()) { + printer().println("No results for filter: " + filterName + ". Did you mean? " + String.join(", ", suggestions)); + } else { + printer().println("No results for filter: " + filterName); + } + printer().println("Tip: use 'camel doc " + filterName + "' for detailed documentation."); } return 0; From d1044c98603769a40a4306b542af249af0c442d0 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 24 Mar 2026 07:57:25 +0100 Subject: [PATCH 05/12] CAMEL-23236: Add camel doctor diagnostic command New `camel doctor` command that checks the environment and reports: - Java version (21+ required) - JBang version - Camel version - Maven Central reachability - Docker daemon status - Common port conflicts (8080, 8443, 9090) - Disk space in temp directory Co-Authored-By: Claude Opus 4.6 --- .../jbang/core/commands/CamelJBangMain.java | 1 + .../camel/dsl/jbang/core/commands/Doctor.java | 136 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Doctor.java diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java index bd03af9ee39c3..3a3f89a0bd3bb 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java @@ -131,6 +131,7 @@ public void execute(String... args) { .addSubcommand("runtime", new CommandLine(new DependencyRuntime(this))) .addSubcommand("update", new CommandLine(new DependencyUpdate(this)))) .addSubcommand("dirty", new CommandLine(new Dirty(this))) + .addSubcommand("doctor", new CommandLine(new Doctor(this))) .addSubcommand("eval", new CommandLine(new EvalCommand(this)) .addSubcommand("expression", new CommandLine(new EvalExpressionCommand(this)))) .addSubcommand("export", new CommandLine(new Export(this))) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Doctor.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Doctor.java new file mode 100644 index 0000000000000..7527f1fc5d53e --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Doctor.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands; + +import java.io.File; +import java.net.HttpURLConnection; +import java.net.URI; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.dsl.jbang.core.common.VersionHelper; +import picocli.CommandLine.Command; + +@Command(name = "doctor", description = "Checks the environment and reports potential issues", + sortOptions = false, showDefaultValues = true) +public class Doctor extends CamelCommand { + + public Doctor(CamelJBangMain main) { + super(main); + } + + @Override + public Integer doCall() throws Exception { + printer().println("Camel JBang Doctor"); + printer().println("=================="); + printer().println(); + + checkJava(); + checkJBang(); + checkCamelVersion(); + checkMavenCentral(); + checkDocker(); + checkCommonPorts(); + checkDiskSpace(); + + return 0; + } + + private void checkJava() { + String version = System.getProperty("java.version"); + String vendor = System.getProperty("java.vendor", ""); + int major = Runtime.version().feature(); + String status = major >= 21 ? "OK" : "WARN (21+ required)"; + printer().printf(" Java: %s (%s) [%s]%n", version, vendor, status); + } + + private void checkJBang() { + String version = VersionHelper.getJBangVersion(); + if (version != null) { + printer().printf(" JBang: %s (OK)%n", version); + } else { + printer().printf(" JBang: not detected%n"); + } + } + + private void checkCamelVersion() { + CamelCatalog catalog = new DefaultCamelCatalog(); + String version = catalog.getCatalogVersion(); + printer().printf(" Camel: %s%n", version); + } + + private void checkMavenCentral() { + try { + HttpURLConnection conn = (HttpURLConnection) URI.create("https://repo1.maven.org/maven2/") + .toURL().openConnection(); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + conn.setRequestMethod("HEAD"); + int code = conn.getResponseCode(); + conn.disconnect(); + printer().printf(" Maven: %s%n", code == 200 ? "reachable (OK)" : "returned " + code); + } catch (Exception e) { + printer().printf(" Maven: unreachable (%s)%n", e.getMessage()); + } + } + + private void checkDocker() { + try { + Process p = new ProcessBuilder("docker", "info") + .redirectErrorStream(true) + .start(); + int exit = p.waitFor(); + printer().printf(" Docker: %s%n", exit == 0 ? "running (OK)" : "not running"); + } catch (Exception e) { + printer().printf(" Docker: not found%n"); + } + } + + private void checkCommonPorts() { + StringBuilder conflicts = new StringBuilder(); + for (int port : new int[] { 8080, 8443, 9090 }) { + if (isPortInUse(port)) { + if (!conflicts.isEmpty()) { + conflicts.append(", "); + } + conflicts.append(port); + } + } + if (!conflicts.isEmpty()) { + printer().printf(" Ports: in use: %s%n", conflicts); + } else { + printer().printf(" Ports: 8080, 8443, 9090 free (OK)%n"); + } + } + + private static boolean isPortInUse(int port) { + try (java.net.ServerSocket ss = new java.net.ServerSocket(port)) { + ss.setReuseAddress(true); + return false; + } catch (Exception e) { + return true; + } + } + + private void checkDiskSpace() { + File tmpDir = new File(System.getProperty("java.io.tmpdir")); + long free = tmpDir.getFreeSpace(); + long mb = free / (1024 * 1024); + String status = mb > 500 ? "OK" : "LOW"; + printer().printf(" Disk space: %d MB free in temp (%s)%n", mb, status); + } +} From 1b1d441987305217046838549c008cbfe705f10a Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 24 Mar 2026 07:57:58 +0100 Subject: [PATCH 06/12] CAMEL-23236: Improve Camel-Kit discoverability - Add KIT to PluginType enum so `camel plugin get --all` shows it - Add tip about camel-kit in `camel init --help` footer - Add AI scaffold hint and examples hint in shell banner when no routes Co-Authored-By: Claude Opus 4.6 --- .../java/org/apache/camel/dsl/jbang/core/commands/Init.java | 3 ++- .../java/org/apache/camel/dsl/jbang/core/commands/Shell.java | 2 ++ .../org/apache/camel/dsl/jbang/core/common/PluginType.java | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java index c4fb6846e2997..1b108bd647b67 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java @@ -51,7 +51,8 @@ import static org.apache.camel.dsl.jbang.core.common.GitHubHelper.fetchGithubUrls; @Command(name = "init", description = "Creates a new Camel integration", - sortOptions = false, showDefaultValues = true) + sortOptions = false, showDefaultValues = true, + footer = "%nTip: For AI-assisted project scaffolding, try: camel plugin add kit") public class Init extends CamelCommand { @Parameters(description = "Name of integration file (or a github link)", arity = "0..1", diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java index df25929c3433a..c86cbd79b8b16 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java @@ -143,8 +143,10 @@ private void printBanner(Terminal terminal) { if (routeCount == 0) { terminal.writer().println("No routes found in current directory."); terminal.writer().println(" Quick start: init MyRoute.yaml && run *"); + terminal.writer().println(" Examples: run --example timer-log"); terminal.writer().println(" Templates: init --list"); terminal.writer().println(" Docs: doc "); + terminal.writer().println(" AI scaffold: plugin add kit"); terminal.writer().println(" Need help? help"); } else { terminal.writer().printf("Found %d route file(s) in current directory.%n", routeCount); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginType.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginType.java index cde0536e15416..2bb6d9b0c73b9 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginType.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginType.java @@ -29,7 +29,9 @@ public enum PluginType { EDIT("edit", "edit", "Edit Camel files with suggestions", "4.12.0", null), TEST("test", "test", "Manage tests for Camel applications", "4.14.0", null), ROUTE_PARSER("route-parser", "route-parser", "Parses Java route and dumps route structure", "4.17.0", null), - VALIDATE("validate", "validate", "Validate Camel routes", "4.18.0", null); + VALIDATE("validate", "validate", "Validate Camel routes", "4.18.0", null), + KIT("kit", "kit", "AI-assisted Camel project scaffolding", "4.19.0", + "https://repo1.maven.org/maven2/"); private final String name; private final String command; From cab2c3f41f5170703811c5567246db27520400c1 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 24 Mar 2026 08:01:17 +0100 Subject: [PATCH 07/12] CAMEL-23236: Fix compilation error and apply formatter Fix List.of() type inference issue with single String[] element. Apply code formatter to PluginType.java. Co-Authored-By: Claude Opus 4.6 --- .../java/org/apache/camel/dsl/jbang/core/commands/Init.java | 5 +++-- .../org/apache/camel/dsl/jbang/core/common/PluginType.java | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java index 1b108bd647b67..3d5f57192ab2d 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java @@ -291,8 +291,9 @@ private int interactivePicker() throws Exception { new String[] { "kamelet-source.yaml", "Kamelet source connector", ".kamelet.yaml" }, new String[] { "kamelet-sink.yaml", "Kamelet sink connector", ".kamelet.yaml" }, new String[] { "kamelet-action.yaml", "Kamelet action processor", ".kamelet.yaml" })); - categories.put("Pipes and CRs", List.of( - new String[] { "init-pipe.yaml", "Pipe CR (source to sink)", ".yaml" })); + List pipeTemplates = new ArrayList<>(); + pipeTemplates.add(new String[] { "init-pipe.yaml", "Pipe CR (source to sink)", ".yaml" }); + categories.put("Pipes and CRs", pipeTemplates); Scanner scanner = new Scanner(System.in); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginType.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginType.java index 2bb6d9b0c73b9..376f2ec8e2d40 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginType.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginType.java @@ -31,7 +31,7 @@ public enum PluginType { ROUTE_PARSER("route-parser", "route-parser", "Parses Java route and dumps route structure", "4.17.0", null), VALIDATE("validate", "validate", "Validate Camel routes", "4.18.0", null), KIT("kit", "kit", "AI-assisted Camel project scaffolding", "4.19.0", - "https://repo1.maven.org/maven2/"); + "https://repo1.maven.org/maven2/"); private final String name; private final String command; From 087bcdb7aed361d5d7e08a7676cc6325968ca4a0 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 24 Mar 2026 08:07:29 +0100 Subject: [PATCH 08/12] CAMEL-23236: Update PluginGetTest for new KIT plugin entry Update expected counts and add assertion for the new KIT plugin. Co-Authored-By: Claude Opus 4.6 --- .../dsl/jbang/core/commands/plugin/PluginGetTest.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginGetTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginGetTest.java index 0bc0f68be52c5..3b9510fb941fa 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginGetTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginGetTest.java @@ -70,7 +70,7 @@ public void shouldGetDefaultPlugins() throws Exception { command.doCall(); List output = printer.getLines(); - Assertions.assertEquals(9, output.size()); + Assertions.assertEquals(10, output.size()); Assertions.assertEquals("Supported plugins:", output.get(0)); Assertions.assertEquals("NAME COMMAND DEPENDENCY DESCRIPTION", output.get(2)); @@ -120,7 +120,7 @@ public void shouldGetAllPlugins() throws Exception { command.doCall(); List output = printer.getLines(); - Assertions.assertEquals(12, output.size()); + Assertions.assertEquals(13, output.size()); Assertions.assertEquals("NAME COMMAND DEPENDENCY DESCRIPTION", output.get(0)); Assertions.assertEquals( "foo-plugin foo org.apache.camel:foo-plugin:1.0.0 Plugin foo-plugin called with command foo", @@ -153,6 +153,10 @@ public void shouldGetAllPlugins() throws Exception { "validate validate org.apache.camel:camel-jbang-plugin-validate %s" .formatted(PluginType.VALIDATE.getDescription()), output.get(11)); + Assertions.assertEquals( + "kit kit org.apache.camel:camel-jbang-plugin-kit %s" + .formatted(PluginType.KIT.getDescription()), + output.get(12)); } } From b828fea682a6adb06b118ed95b5d1045fe50d2d6 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 24 Mar 2026 08:11:32 +0100 Subject: [PATCH 09/12] CAMEL-23236: Code review fixes - Doctor: drain docker process output to prevent blocking, use import for ServerSocket instead of fully-qualified name - Init: handle NumberFormatException in interactive picker with better error messages - Shell: add parentheses to boolean logic in countRouteFiles for clarity Co-Authored-By: Claude Opus 4.6 --- .superset/config.json | 7 +++++++ .../camel/dsl/jbang/core/commands/Doctor.java | 5 ++++- .../camel/dsl/jbang/core/commands/Init.java | 20 +++++++++++++++---- .../camel/dsl/jbang/core/commands/Shell.java | 6 +++--- 4 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 .superset/config.json diff --git a/.superset/config.json b/.superset/config.json new file mode 100644 index 0000000000000..d411f86a050bc --- /dev/null +++ b/.superset/config.json @@ -0,0 +1,7 @@ +{ + "setup": [ + "git submodule update --init --recursive" + ], + "teardown": [], + "run": [] +} \ No newline at end of file diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Doctor.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Doctor.java index 7527f1fc5d53e..aa63167585d9b 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Doctor.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Doctor.java @@ -18,6 +18,7 @@ import java.io.File; import java.net.HttpURLConnection; +import java.net.ServerSocket; import java.net.URI; import org.apache.camel.catalog.CamelCatalog; @@ -93,6 +94,8 @@ private void checkDocker() { Process p = new ProcessBuilder("docker", "info") .redirectErrorStream(true) .start(); + // drain output to prevent blocking + p.getInputStream().transferTo(java.io.OutputStream.nullOutputStream()); int exit = p.waitFor(); printer().printf(" Docker: %s%n", exit == 0 ? "running (OK)" : "not running"); } catch (Exception e) { @@ -118,7 +121,7 @@ private void checkCommonPorts() { } private static boolean isPortInUse(int port) { - try (java.net.ServerSocket ss = new java.net.ServerSocket(port)) { + try (ServerSocket ss = new ServerSocket(port)) { ss.setReuseAddress(true); return false; } catch (Exception e) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java index 3d5f57192ab2d..68fdd596e5dc3 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java @@ -305,9 +305,15 @@ private int interactivePicker() throws Exception { } printer().print("Choice [1]: "); String categoryInput = scanner.nextLine().trim(); - int categoryIdx = categoryInput.isEmpty() ? 0 : Integer.parseInt(categoryInput) - 1; + int categoryIdx; + try { + categoryIdx = categoryInput.isEmpty() ? 0 : Integer.parseInt(categoryInput) - 1; + } catch (NumberFormatException e) { + printer().printErr("Invalid choice: " + categoryInput); + return 1; + } if (categoryIdx < 0 || categoryIdx >= categoryNames.size()) { - printer().printErr("Invalid choice."); + printer().printErr("Invalid choice: must be between 1 and " + categoryNames.size()); return 1; } @@ -321,9 +327,15 @@ private int interactivePicker() throws Exception { } printer().print("Choice [1]: "); String templateInput = scanner.nextLine().trim(); - int templateIdx = templateInput.isEmpty() ? 0 : Integer.parseInt(templateInput) - 1; + int templateIdx; + try { + templateIdx = templateInput.isEmpty() ? 0 : Integer.parseInt(templateInput) - 1; + } catch (NumberFormatException e) { + printer().printErr("Invalid choice: " + templateInput); + return 1; + } if (templateIdx < 0 || templateIdx >= templates.size()) { - printer().printErr("Invalid choice."); + printer().printErr("Invalid choice: must be between 1 and " + templates.size()); return 1; } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java index c86cbd79b8b16..22252fba122a7 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java @@ -163,9 +163,9 @@ private static int countRouteFiles() { count = (int) files.filter(Files::isRegularFile) .filter(p -> { String name = p.getFileName().toString(); - return name.endsWith(".yaml") && !name.endsWith(".kamelet.yaml") - && !name.equals("application.yaml") - || name.endsWith(".xml") && !name.equals("pom.xml") + return (name.endsWith(".yaml") && !name.endsWith(".kamelet.yaml") + && !name.equals("application.yaml")) + || (name.endsWith(".xml") && !name.equals("pom.xml")) || name.endsWith(".java"); }) .count(); From 65d7843822ca0cf6bef4d5c8f6b6799ef1b81b58 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 24 Mar 2026 08:11:36 +0100 Subject: [PATCH 10/12] Add .superset/ to .gitignore Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + .superset/config.json | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 .superset/config.json diff --git a/.gitignore b/.gitignore index 7193719404e3e..7c08a6793bec5 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ mvnd.zip* .mvn/.develocity/develocity-workspace-id backlog .claude +.superset/ diff --git a/.superset/config.json b/.superset/config.json deleted file mode 100644 index d411f86a050bc..0000000000000 --- a/.superset/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "setup": [ - "git submodule update --init --recursive" - ], - "teardown": [], - "run": [] -} \ No newline at end of file From b44f1ba3e5b4400e7415bc92da719abbb3192a37 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 24 Mar 2026 08:11:47 +0100 Subject: [PATCH 11/12] Revert "Add .superset/ to .gitignore" This reverts commit 65d7843822ca0cf6bef4d5c8f6b6799ef1b81b58. --- .gitignore | 1 - .superset/config.json | 7 +++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .superset/config.json diff --git a/.gitignore b/.gitignore index 7c08a6793bec5..7193719404e3e 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,3 @@ mvnd.zip* .mvn/.develocity/develocity-workspace-id backlog .claude -.superset/ diff --git a/.superset/config.json b/.superset/config.json new file mode 100644 index 0000000000000..d411f86a050bc --- /dev/null +++ b/.superset/config.json @@ -0,0 +1,7 @@ +{ + "setup": [ + "git submodule update --init --recursive" + ], + "teardown": [], + "run": [] +} \ No newline at end of file From 560bd5dd2cfd093cb789ea8c773ba92666caf4b8 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 24 Mar 2026 08:12:01 +0100 Subject: [PATCH 12/12] CAMEL-23236: Remove accidentally tracked .superset/config.json Co-Authored-By: Claude Opus 4.6 --- .superset/config.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .superset/config.json diff --git a/.superset/config.json b/.superset/config.json deleted file mode 100644 index d411f86a050bc..0000000000000 --- a/.superset/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "setup": [ - "git submodule update --init --recursive" - ], - "teardown": [], - "run": [] -} \ No newline at end of file