This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 826a664c5f16 CAMEL-23688: camel-jbang - camel init does not overwrite 
a file without --force (#27995)
826a664c5f16 is described below

commit 826a664c5f165c15a9ac7abada64707a3e4afe8e
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 17 09:09:43 2026 +0200

    CAMEL-23688: camel-jbang - camel init does not overwrite a file without 
--force (#27995)
    
    camel init exits with 1 and leaves the file as it is when the file it 
creates exists already, and the new option --force overwrites it. The files 
downloaded from GitHub or a gist are all checked before any is written. When 
the template is picked in the terminal, camel init asks there whether to 
overwrite the file (no by default).
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  7 ++
 .../pages/jbang-commands/camel-jbang-init.adoc     |  1 +
 .../jbang-commands/camel-jbang-test-init.adoc      |  1 +
 .../META-INF/camel-jbang-commands-metadata.json    |  2 +-
 .../apache/camel/dsl/jbang/core/commands/Init.java | 75 ++++++++++++++-----
 .../camel/dsl/jbang/core/commands/InitTest.java    | 84 ++++++++++++++++++++++
 .../camel-jbang-plugin-commands-metadata.json      |  2 +-
 7 files changed, 153 insertions(+), 19 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 81cf732576ea..974fba740871 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -587,6 +587,13 @@ the integrations unsorted. A row without the value of the 
key now comes first, i
 unknown `--type` exits with `2` (it showed nothing), a `--filter` that matches 
nothing says so on stderr, and `--json`
 prints the Kamelets as JSON, as the other `camel catalog` commands do.
 
+=== camel-jbang - camel init does not overwrite a file
+
+`camel init` exits with `1` when the file it creates exists already, and 
leaves the file as it is (it overwrote the
+file before). Use `--force` to overwrite it. The same holds for the files 
`camel init` downloads from GitHub or a gist,
+of which it writes none when one of them exists. When the template is picked 
in the terminal (`camel init` without a
+file), the command asks there whether to overwrite the file.
+
 === camel-jbang - camel get history
 
 * `camel get history --diagram` and `--it` exit with `2` when several 
integrations have a message history, and ask for
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-init.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-init.adoc
index f9792e234294..939d243f86a2 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-init.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-init.adoc
@@ -21,6 +21,7 @@ camel init [options]
 | Option | Description | Default | Type
 | `--clean-dir,--clean-directory` | Whether to clean directory first (deletes 
all files in directory) |  | boolean
 | `--dir,--directory` | Directory relative path where the new Camel 
integration will be saved | . | String
+| `--force` | Overwrite a file that exists already (by default the command 
fails instead) |  | boolean
 | `--from-kamelet` | To be used when extending an existing Kamelet |  | String
 | `--kamelets-version` | Apache Camel Kamelets version (auto-detected from 
classpath if not set) |  | String
 | `--list` | List available templates |  | boolean
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-init.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-init.adoc
index 788e53487b56..b8f803053c14 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-init.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-test-init.adoc
@@ -23,6 +23,7 @@ camel test init [options]
 | Option | Description | Default | Type
 | `--clean-dir,--clean-directory` | Whether to clean directory first (deletes 
all files in directory) |  | boolean
 | `--dir,--directory` | Directory relative path where the new Camel 
integration will be saved | . | String
+| `--force` | Overwrite a file that exists already (by default the command 
fails instead) |  | boolean
 | `--from-kamelet` | To be used when extending an existing Kamelet |  | String
 | `--kamelets-version` | Apache Camel Kamelets version (auto-detected from 
classpath if not set) |  | String
 | `--list` | List available templates |  | boolean
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index b26b0f3e4961..fcf519c28b5a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -19,7 +19,7 @@
     { "name": "harden", "fullName": "harden", "description": "Suggest security 
hardening for Camel routes using AI\/LLM", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Harden", "options": [ { "names": 
"--api-key", "description": "API key for authentication. Also reads 
OPENAI_API_KEY or LLM_API_KEY env vars", "javaType": "java.lang.String", 
"type": "string" }, { "names": "--api-type", "description": "API type: 'ollama' 
or 'openai' (OpenAI-compatible)", "defaultValue": "ollama", [...]
     { "name": "hawtio", "fullName": "hawtio", "description": "Launch Hawtio 
web console", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.process.Hawtio", "options": [ { 
"names": "--host", "description": "Hostname to bind the Hawtio web console to", 
"defaultValue": "127.0.0.1", "javaType": "java.lang.String", "type": "string" 
}, { "names": "--open-url,--openUrl", "description": "To automatic open Hawtio 
web console in the web browser (without an interactive terminal the default  
[...]
     { "name": "infra", "fullName": "infra", "description": "List and Run 
external services for testing and prototyping", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.infra.InfraCommand", "options": [ { 
"names": "--json", "description": "Output in JSON Format", "javaType": 
"boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display 
the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], 
"subcommands": [ { "name": "get", "fullName": "infra  [...]
-    { "name": "init", "fullName": "init", "description": "Creates a new Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Init", 
"options": [ { "names": "--clean-dir,--clean-directory", "description": 
"Whether to clean directory first (deletes all files in directory)", 
"javaType": "boolean", "type": "boolean" }, { "names": "--dir,--directory", 
"description": "Directory relative path where the new Camel integration will be 
saved", "defaultValue": ".", "javaType" [...]
+    { "name": "init", "fullName": "init", "description": "Creates a new Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Init", 
"options": [ { "names": "--clean-dir,--clean-directory", "description": 
"Whether to clean directory first (deletes all files in directory)", 
"javaType": "boolean", "type": "boolean" }, { "names": "--dir,--directory", 
"description": "Directory relative path where the new Camel integration will be 
saved", "defaultValue": ".", "javaType" [...]
     { "name": "jolokia", "fullName": "jolokia", "description": "Attach Jolokia 
JVM Agent to a running Camel integration", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.process.Jolokia", "options": [ { 
"names": "--port", "description": "To use a specific port number when attaching 
Jolokia JVM Agent (default a free port is found in range 8778-9999)", 
"javaType": "int", "type": "integer" }, { "names": "--stop", "description": 
"Stops the Jolokia JVM Agent in the running Camel inte [...]
     { "name": "log", "fullName": "log", "description": "Tail logs from running 
Camel integrations", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelLogAction", "options": [ 
{ "names": "--duration", "description": "How long to keep watching or 
following, in millis or as a duration such as 30s (default: until Enter is 
pressed)", "javaType": "long", "type": "integer" }, { "names": "--find", 
"description": "Find and highlight matching text (ignore case).", "javaType": 
"j [...]
     { "name": "nano", "fullName": "nano", "description": "Nano editor to edit 
file", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Nano", 
"options": [ { "names": "-h,--help", "description": "Display the help and 
sub-commands", "javaType": "boolean", "type": "boolean" } ] },
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 8bb3caf32a0c..bc09b6c67446 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
@@ -81,6 +81,10 @@ public class Init extends CamelCommand {
             description = "Whether to clean directory first (deletes all files 
in directory)")
     private boolean cleanDirectory;
 
+    @Option(names = { "--force" },
+            description = "Overwrite a file that exists already (by default 
the command fails instead)")
+    private boolean force;
+
     @Option(names = { "--from-kamelet" },
             description = "To be used when extending an existing Kamelet")
     private String fromKamelet;
@@ -97,6 +101,9 @@ public class Init extends CamelCommand {
             description = "Additional maven repositories (Use commas to 
separate multiple repositories)")
     protected String repositories;
 
+    // when the template is picked in the terminal, where to ask whether to 
overwrite a file that exists already
+    private Scanner overwritePrompt;
+
     public Init(CamelJBangMain main) {
         super(main);
     }
@@ -109,7 +116,8 @@ public class Init extends CamelCommand {
         if (file == null) {
             // try interactive picker if running in a TTY and not in CI
             if (EnvironmentHelper.isInteractiveTerminal()) {
-                return interactivePicker();
+                Terminal activeTerminal = 
EnvironmentHelper.getActiveTerminal();
+                return interactivePicker(activeTerminal != null ? 
activeTerminal.input() : System.in);
             }
             printer().printErr("Missing required parameter: <file>");
             printer().printTip("Run 'camel init --list' to see available 
templates, or run interactively in a terminal.");
@@ -232,10 +240,50 @@ public class Init extends CamelCommand {
                 Files.createDirectories(parentPath);
             }
         }
+        if (existsAlready(targetPath)) {
+            return CliExitCode.FAILURE;
+        }
         Files.writeString(targetPath, content);
         return 0;
     }
 
+    /**
+     * Whether the file is there already and not to be overwritten (without 
--force), which is then an error. When the
+     * template is picked in the terminal, the user is asked there whether to 
overwrite the file.
+     */
+    private boolean existsAlready(Path targetPath) {
+        if (force || !Files.exists(targetPath)) {
+            return false;
+        }
+        if (overwritePrompt != null) {
+            printer().printf("File already exists: %s, overwrite it? [y/N]: ", 
targetPath);
+            String answer = overwritePrompt.hasNextLine() ? 
overwritePrompt.nextLine().trim() : "";
+            if ("y".equalsIgnoreCase(answer) || 
"yes".equalsIgnoreCase(answer)) {
+                return false;
+            }
+        }
+        printer().printErr("File already exists: " + targetPath + " (use 
--force to overwrite it)");
+        return true;
+    }
+
+    /**
+     * Writes the downloaded files, and none of them when one of them is there 
already (without --force).
+     */
+    private int writeDownloaded(List<Resource> resources) throws Exception {
+        for (Resource resource : resources) {
+            if (existsAlready(Paths.get(directory, 
FileUtil.stripPath(resource.getLocation())))) {
+                return CliExitCode.FAILURE;
+            }
+        }
+        for (Resource resource : resources) {
+            Path targetPath = Paths.get(directory, 
FileUtil.stripPath(resource.getLocation()));
+            try (OutputStream os = Files.newOutputStream(targetPath)) {
+                IOUtils.copy(resource.getInputStream(), os);
+            }
+        }
+        return 0;
+    }
+
     /**
      * @return The package declaration lines to insert at the beginning of the 
file or empty string if no package found
      */
@@ -297,7 +345,7 @@ public class Init extends CamelCommand {
         return 0;
     }
 
-    private int interactivePicker() throws Exception {
+    int interactivePicker(InputStream input) throws Exception {
         // Build template categories
         Map<String, List<String[]>> categories = new LinkedHashMap<>();
         categories.put("Routes", List.of(
@@ -312,9 +360,7 @@ public class Init extends CamelCommand {
         pipeTemplates.add(new String[] { "init-pipe.yaml", "Pipe CR (source to 
sink)", ".yaml" });
         categories.put("Pipes and CRs", pipeTemplates);
 
-        Terminal activeTerminal = EnvironmentHelper.getActiveTerminal();
-        InputStream scannerInput = activeTerminal != null ? 
activeTerminal.input() : System.in;
-        Scanner scanner = new Scanner(scannerInput);
+        Scanner scanner = new Scanner(input);
 
         // Step 1: Pick a category
         printer().println("Select a template category:");
@@ -383,6 +429,7 @@ public class Init extends CamelCommand {
         }
 
         this.file = filename;
+        overwritePrompt = scanner;
         int code = execute();
         if (code == 0) {
             createWorkingDirectoryIfAbsent();
@@ -435,18 +482,15 @@ public class Init extends CamelCommand {
             CamelContext tiny = new DefaultCamelContext();
             try (GitHubResourceResolver resolver = new 
GitHubResourceResolver()) {
                 resolver.setCamelContext(tiny);
+                List<Resource> resources = new ArrayList<>();
                 for (String u : all.toString().split(",")) {
                     Resource resource = resolver.resolve(u);
                     if (!resource.exists()) {
                         throw new ResourceDoesNotExist(resource);
                     }
-                    String loc = resource.getLocation();
-                    String name = FileUtil.stripPath(loc);
-                    Path targetPath = Paths.get(directory, name);
-                    try (OutputStream os = Files.newOutputStream(targetPath)) {
-                        IOUtils.copy(resource.getInputStream(), os);
-                    }
+                    resources.add(resource);
                 }
+                return writeDownloaded(resources);
             }
         }
 
@@ -472,18 +516,15 @@ public class Init extends CamelCommand {
             CamelContext tiny = new DefaultCamelContext();
             try (GistResourceResolver resolver = new GistResourceResolver()) {
                 resolver.setCamelContext(tiny);
+                List<Resource> resources = new ArrayList<>();
                 for (String u : all.toString().split(",")) {
                     Resource resource = resolver.resolve(u);
                     if (!resource.exists()) {
                         throw new ResourceDoesNotExist(resource);
                     }
-                    String loc = resource.getLocation();
-                    String name = FileUtil.stripPath(loc);
-                    Path targetPath = Paths.get(directory, name);
-                    try (OutputStream os = Files.newOutputStream(targetPath)) {
-                        IOUtils.copy(resource.getInputStream(), os);
-                    }
+                    resources.add(resource);
                 }
+                return writeDownloaded(resources);
             }
         }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/InitTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/InitTest.java
index 5ae9694efb97..606442bf933b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/InitTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/InitTest.java
@@ -16,7 +16,10 @@
  */
 package org.apache.camel.dsl.jbang.core.commands;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -196,6 +199,83 @@ class InitTest {
         
assertTrue(Files.readString(workingDir.resolve("my.yaml")).contains("kind: 
Pipe"));
     }
 
+    @Test
+    void initFailsWhenTheFileExists() throws Exception {
+        Path existing = Files.writeString(workingDir.resolve("my.camel.yaml"), 
"- route: {}");
+        StringPrinter printer = new StringPrinter();
+        Init initCommand = new Init(new CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(initCommand, "my.camel.yaml", "--dir=" + 
workingDir);
+
+        int exit = initCommand.doCall();
+
+        assertEquals(1, exit);
+        assertEquals("ERROR: File already exists: " + existing + " (use 
--force to overwrite it)", printer.getStderr());
+        assertEquals("- route: {}", Files.readString(existing), "the file is 
kept as it was");
+    }
+
+    @Test
+    void initForceOverwritesTheFile() throws Exception {
+        Path existing = Files.writeString(workingDir.resolve("my.camel.yaml"), 
"- route: {}");
+        // the same command without --force keeps the file, so it is --force 
that overwrites it
+        Init withoutForce = new Init(new CamelJBangMain().withPrinter(new 
StringPrinter()));
+        CommandLine.populateCommand(withoutForce, "my.camel.yaml", "--dir=" + 
workingDir);
+        assertEquals(1, withoutForce.doCall());
+        assertEquals("- route: {}", Files.readString(existing));
+
+        Init initCommand = new Init(new CamelJBangMain());
+        CommandLine.populateCommand(initCommand, "my.camel.yaml", "--dir=" + 
workingDir, "--force");
+
+        int exit = initCommand.doCall();
+
+        assertEquals(0, exit);
+        assertTrue(Files.readString(existing).contains("from:"), "the file is 
the template now");
+    }
+
+    @Test
+    void initPickerAsksBeforeOverwritingTheFile() throws Exception {
+        Path existing = Files.writeString(workingDir.resolve("my.camel.yaml"), 
"- route: {}");
+        StringPrinter printer = new StringPrinter();
+        Init initCommand = new Init(new CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(initCommand, "--dir=" + workingDir);
+
+        // the YAML route template, the file, and yes to overwriting it
+        int exit = initCommand.interactivePicker(input("1", "1", 
"my.camel.yaml", "y"));
+
+        assertEquals(0, exit);
+        assertTrue(printer.getStdout().contains("File already exists: " + 
existing + ", overwrite it? [y/N]: "));
+        assertTrue(Files.readString(existing).contains("from:"), "the file is 
the template now");
+    }
+
+    @Test
+    void initPickerWithForceDoesNotAsk() throws Exception {
+        Path existing = Files.writeString(workingDir.resolve("my.camel.yaml"), 
"- route: {}");
+        StringPrinter printer = new StringPrinter();
+        Init initCommand = new Init(new CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(initCommand, "--dir=" + workingDir, 
"--force");
+
+        // no answer to give, as there is no question
+        int exit = initCommand.interactivePicker(input("1", "1", 
"my.camel.yaml"));
+
+        assertEquals(0, exit);
+        assertFalse(printer.getStdout().contains("overwrite it?"), 
printer.getStdout());
+        assertTrue(Files.readString(existing).contains("from:"), "the file is 
the template now");
+    }
+
+    @Test
+    void initPickerKeepsTheFileWhenTheAnswerIsNo() throws Exception {
+        Path existing = Files.writeString(workingDir.resolve("my.camel.yaml"), 
"- route: {}");
+        StringPrinter printer = new StringPrinter();
+        Init initCommand = new Init(new CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(initCommand, "--dir=" + workingDir);
+
+        // no answer is no
+        int exit = initCommand.interactivePicker(input("1", "1", 
"my.camel.yaml", ""));
+
+        assertEquals(1, exit);
+        assertEquals("ERROR: File already exists: " + existing + " (use 
--force to overwrite it)", printer.getStderr());
+        assertEquals("- route: {}", Files.readString(existing), "the file is 
kept as it was");
+    }
+
     @Test
     void initCleansTheDirectoryFirst() throws Exception {
         Path dir = Files.createDirectories(workingDir.resolve("project"));
@@ -269,4 +349,8 @@ class InitTest {
         assertTrue(printer.getStderr().startsWith("ERROR: Missing required 
parameter: <file>"), printer.getStderr());
         assertEquals("", printer.getStdout());
     }
+
+    private static InputStream input(String... lines) {
+        return new ByteArrayInputStream((String.join("\n", lines) + 
"\n").getBytes(StandardCharsets.UTF_8));
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-test/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-plugin-test/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
index f90720a6d5bd..36d3c46b9e14 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-test/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-test/src/generated/resources/META-INF/camel-jbang-plugin-commands-metadata.json
@@ -1,5 +1,5 @@
 {
   "commands": [
-    { "name": "test", "fullName": "test", "description": "Manage tests for 
Camel applications", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.test.TestCommand", "plugin": "test", 
"options": [ { "names": "-h,--help", "description": "Display the help and 
sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { 
"name": "init", "fullName": "test init", "description": "Creates a new Citrus 
test", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.test.T [...]
+    { "name": "test", "fullName": "test", "description": "Manage tests for 
Camel applications", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.test.TestCommand", "plugin": "test", 
"options": [ { "names": "-h,--help", "description": "Display the help and 
sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { 
"name": "init", "fullName": "test init", "description": "Creates a new Citrus 
test", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.test.T [...]
   ]
 }

Reply via email to