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

davsclaus pushed a commit to branch fix/CAMEL-23688-init-force
in repository https://gitbox.apache.org/repos/asf/camel.git

commit c37dbf66bebe8de38e81a9e6bc9086dac35045b3
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 17 08:08:58 2026 +0200

    CAMEL-23688: camel-jbang - camel init does not overwrite a file without 
--force
    
    camel init overwrote the file it creates when it existed already, such as
    a route someone had been working on. Now it exits with 1 and leaves the
    file as it is, and the new --force option overwrites it. The files
    camel init downloads from GitHub or a gist are checked first, and none
    is written when one of them exists.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  6 +++
 .../pages/jbang-commands/camel-jbang-init.adoc     |  1 +
 .../META-INF/camel-jbang-commands-metadata.json    |  2 +-
 .../apache/camel/dsl/jbang/core/commands/Init.java | 54 +++++++++++++++++-----
 .../camel/dsl/jbang/core/commands/InitTest.java    | 26 +++++++++++
 5 files changed, 76 insertions(+), 13 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..9f928ac56dca 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,12 @@ 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 it.
+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.
+
 === 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/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 ee4d49be23bb..bec964991348 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..4c8a6bf589db 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;
@@ -232,10 +236,42 @@ 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.
+     */
+    private boolean existsAlready(Path targetPath) {
+        if (!force && Files.exists(targetPath)) {
+            printer().printErr("File already exists: " + targetPath + " (use 
--force to overwrite it)");
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 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
      */
@@ -435,18 +471,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 +505,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..b08b92803548 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
@@ -196,6 +196,32 @@ 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: {}");
+        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 initCleansTheDirectoryFirst() throws Exception {
         Path dir = Files.createDirectories(workingDir.resolve("project"));

Reply via email to