This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch camel-23236-beginner-ux in repository https://gitbox.apache.org/repos/asf/camel.git
commit 946c890e8519fec225f3d4b1147c33f57bba3268 Author: Guillaume Nodet <[email protected]> AuthorDate: Tue May 5 16:54:29 2026 +0200 CAMEL-23236: Improve beginner UX with interactive init, examples, doctor, and contextual help - Add interactive template picker to camel init (--list flag, arrow-key selection) - Add camel run --example for zero-to-running experience with bundled examples - Add camel doctor diagnostic command for environment/dependency checks - Extend did-you-mean suggestions to more commands in CatalogBaseCommand - Add context-aware shell banner (detects route files, shows quick-start hints) - Improve Camel-Kit discoverability (add KIT plugin type) - Include bundled example route files (timer-log, cron-log, rest-api) Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../pages/jbang-commands/camel-jbang-commands.adoc | 1 + .../pages/jbang-commands/camel-jbang-debug.adoc | 2 + .../pages/jbang-commands/camel-jbang-doctor.adoc | 25 ++++ .../ROOT/pages/jbang-commands/camel-jbang-run.adoc | 2 + .../dsl/jbang/core/commands/CamelJBangMain.java | 1 + .../camel/dsl/jbang/core/commands/Doctor.java | 150 +++++++++++++++++++++ .../apache/camel/dsl/jbang/core/commands/Init.java | 109 ++++++++++++++- .../apache/camel/dsl/jbang/core/commands/Run.java | 71 ++++++++++ .../camel/dsl/jbang/core/commands/Shell.java | 31 ++++- .../core/commands/catalog/CatalogBaseCommand.java | 11 ++ .../camel/dsl/jbang/core/common/PluginType.java | 2 + .../src/main/resources/examples/cron-log.yaml | 10 ++ .../src/main/resources/examples/rest-api.yaml | 21 +++ .../src/main/resources/examples/timer-log.yaml | 10 ++ .../jbang/core/commands/plugin/PluginGetTest.java | 12 +- 15 files changed, 454 insertions(+), 4 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc index 8d9e0406c6a9..5425cf41a836 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc @@ -20,6 +20,7 @@ TIP: You can also use `camel --help` or `camel <command> --help` to see availabl | xref:jbang-commands/camel-jbang-dependency.adoc[camel dependency] | Displays all Camel dependencies required to run | xref:jbang-commands/camel-jbang-dirty.adoc[camel dirty] | Check if there are dirty files from previous Camel runs that did not terminate gracefully | xref:jbang-commands/camel-jbang-doc.adoc[camel doc] | Shows documentation for kamelet, component, and other Camel resources +| xref:jbang-commands/camel-jbang-doctor.adoc[camel doctor] | Checks the environment and reports potential issues | xref:jbang-commands/camel-jbang-eval.adoc[camel eval] | Evaluate Camel expressions and scripts | xref:jbang-commands/camel-jbang-explain.adoc[camel explain] | Explain what a Camel route does using AI/LLM | xref:jbang-commands/camel-jbang-export.adoc[camel export] | Export to other runtimes (Camel Main, Spring Boot, or Quarkus) diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc index a545282a1cd8..2ba7c40779cd 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc @@ -31,6 +31,8 @@ camel debug [options] | `--dep,--dependency` | Add additional dependencies | | List | `--download` | Whether to allow automatic downloading JAR dependencies (over the internet) | true | boolean | `--empty` | Run an empty Camel without loading source files | false | boolean +| `--example` | Run a built-in example by name (e.g., timer-log, rest-api). Use --example --list to show available examples. | | String +| `--example-list` | List available built-in examples | | boolean | `--exclude` | Exclude files by name or pattern | | List | `--fresh` | Make sure we use fresh (i.e. non-cached) resources | false | boolean | `--gav` | The Maven group:artifact:version (used during exporting) | | String diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-doctor.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-doctor.adoc new file mode 100644 index 000000000000..dcf0ad911535 --- /dev/null +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-doctor.adoc @@ -0,0 +1,25 @@ + +// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE += camel doctor + +Checks the environment and reports potential issues + + +== Usage + +[source,bash] +---- +camel doctor [options] +---- + + + +== Options + +[cols="2,5,1,2",options="header"] +|=== +| Option | Description | Default | Type +| `-h,--help` | Display the help and sub-commands | | boolean +|=== + + diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc index 61648266fcaa..936fd1385e3a 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc @@ -29,6 +29,8 @@ camel run [options] | `--dep,--dependency` | Add additional dependencies | | List | `--download` | Whether to allow automatic downloading JAR dependencies (over the internet) | true | boolean | `--empty` | Run an empty Camel without loading source files | false | boolean +| `--example` | Run a built-in example by name (e.g., timer-log, rest-api). Use --example --list to show available examples. | | String +| `--example-list` | List available built-in examples | | boolean | `--exclude` | Exclude files by name or pattern | | List | `--fresh` | Make sure we use fresh (i.e. non-cached) resources | false | boolean | `--gav` | The Maven group:artifact:version (used during exporting) | | String 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 477769c86125..d96725ab5749 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 @@ -132,6 +132,7 @@ public class CamelJBangMain implements Callable<Integer> { .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 000000000000..e048233320b1 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Doctor.java @@ -0,0 +1,150 @@ +/* + * 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.ServerSocket; +import java.util.List; +import java.util.Set; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.dsl.jbang.core.common.VersionHelper; +import org.apache.camel.tooling.maven.MavenDownloaderImpl; +import org.apache.camel.tooling.maven.MavenResolutionException; +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(); + checkMavenRepository(); + checkContainerRuntime(); + 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 checkMavenRepository() { + MavenDownloaderImpl downloader = new MavenDownloaderImpl(); + try { + downloader.build(); + CamelCatalog catalog = new DefaultCamelCatalog(); + String version = catalog.getCatalogVersion(); + downloader.resolveArtifacts( + List.of("org.apache.camel:camel-api:" + version), + Set.of(), false, false); + printer().printf(" Maven: artifact resolution OK%n"); + } catch (MavenResolutionException e) { + printer().printf(" Maven: artifact resolution failed (%s)%n", e.getMessage()); + } catch (Exception e) { + printer().printf(" Maven: error (%s)%n", e.getMessage()); + } + } + + private void checkContainerRuntime() { + // check docker first, then podman as fallback + for (String cmd : new String[] { "docker", "podman" }) { + try { + Process p = new ProcessBuilder(cmd, "info") + .redirectErrorStream(true) + .start(); + // drain output to prevent blocking + p.getInputStream().transferTo(java.io.OutputStream.nullOutputStream()); + int exit = p.waitFor(); + if (exit == 0) { + printer().printf(" Container: %s running (OK, optional)%n", cmd); + return; + } + } catch (Exception e) { + // not found, try next + } + } + printer().printf(" Container: not found (optional — needed for test containers)%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 (ServerSocket ss = new 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); + } +} 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 e3e3f42ca037..60d939fad10c 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 @@ -23,8 +23,11 @@ import java.io.OutputStream; 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; @@ -55,7 +58,8 @@ import static org.apache.camel.dsl.jbang.core.common.GitHubHelper.fetchGithubUrl " camel init hello.java", " camel init hello.yaml", " camel init hello.xml", - " camel init --list" }) + " camel init --list", + "%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", @@ -102,7 +106,12 @@ public class Init extends CamelCommand { 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: <file>"); + printer().printErr("Run 'camel init --list' to see available templates, or run interactively in a terminal."); return 1; } int code = execute(); @@ -286,6 +295,104 @@ public class Init extends CamelCommand { return 0; } + private int interactivePicker() throws Exception { + // Build template categories + Map<String, List<String[]>> 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" })); + List<String[]> 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); + + // Step 1: Pick a category + printer().println("Select a template category:"); + List<String> 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; + 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: must be between 1 and " + categoryNames.size()); + return 1; + } + + // Step 2: Pick a template + String selectedCategory = categoryNames.get(categoryIdx); + List<String[]> 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; + 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: must be between 1 and " + templates.size()); + 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 <component>"); + } + return code; + } + private void createWorkingDirectoryIfAbsent() { Path work = CommandLineHelper.getWorkDir(); if (!Files.exists(work)) { 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 83383946d44c..75fee0fd553b 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 @@ -336,6 +336,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); } @@ -355,6 +364,14 @@ public class Run extends CamelCommand { @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:"); } @@ -362,6 +379,60 @@ public class Run extends CamelCommand { 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 <name>"); + printer().println(" camel run --example <name> --dev"); + return 0; + } + + private static final List<String> 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) { + List<String> 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; + } + + // 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/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 8cb436ccbde7..715b7d76b052 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,9 +16,11 @@ */ 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.stream.Stream; import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; import org.apache.camel.dsl.jbang.core.common.VersionHelper; @@ -151,8 +153,35 @@ public class Shell extends CamelCommand { } writer.println(banner); } - writer.println("Type 'help' for available commands, 'exit' to quit."); + int routeCount = countRouteFiles(); + if (routeCount == 0) { + writer.println("No routes found in current directory."); + writer.println(" Quick start: init MyRoute.yaml && run *"); + writer.println(" Templates: init --list"); + writer.println(" Docs: doc <component>"); + writer.println(" Need help? help"); + } else { + writer.printf("Found %d route file(s) in current directory.%n", routeCount); + writer.println(" Run: run *"); + writer.println(" Watch: run * --dev"); + } writer.println(); writer.flush(); } + + private static int countRouteFiles() { + try (Stream<Path> files = Files.list(Paths.get("."))) { + return (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) { + return 0; + } + } } 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 92acba5edae6..a0d2748b5e2c 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 @@ -37,6 +37,7 @@ import org.apache.camel.dsl.jbang.core.common.RuntimeTypeConverter; import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper; 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; @@ -180,6 +181,16 @@ public abstract class CatalogBaseCommand extends CamelCommand { .maxWidth(descWidth, OverflowBehaviour.ELLIPSIS_RIGHT) .with(this::shortDescription)))); } + } else if (filterName != null) { + // suggest similar names when filter returns no results + List<String> allNames = collectRows().stream().map(r -> r.name).collect(Collectors.toList()); + List<String> 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; 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 0e3eb1d5df4c..63a475b3fb71 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 @@ -30,6 +30,8 @@ public enum PluginType { 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), + KIT("kit", "kit", "AI-assisted Camel project scaffolding", "4.19.0", + "https://repo1.maven.org/maven2/"), TUI("tui", "tui", "Camel Dashboard", "4.20.0", null); private final String name; 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 000000000000..c31bdb1cec3d --- /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 000000000000..2e1035cf1f0f --- /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 000000000000..cdff71b4d71f --- /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}" 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 15004d080ab9..bd72e594cb24 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 @@ class PluginGetTest extends CamelCommandBaseTestSupport { command.doCall(); List<String> output = printer.getLines(); - Assertions.assertEquals(10, output.size()); + Assertions.assertEquals(11, output.size()); Assertions.assertEquals("Supported plugins:", output.get(0)); Assertions.assertEquals("NAME COMMAND DEPENDENCY DESCRIPTION", output.get(2)); @@ -120,7 +120,7 @@ class PluginGetTest extends CamelCommandBaseTestSupport { command.doCall(); List<String> output = printer.getLines(); - Assertions.assertEquals(13, output.size()); + Assertions.assertEquals(14, 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,14 @@ class PluginGetTest extends CamelCommandBaseTestSupport { "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)); + Assertions.assertEquals( + "tui tui org.apache.camel:camel-jbang-plugin-tui %s" + .formatted(PluginType.TUI.getDescription()), + output.get(13)); } }
