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 [...]
]
}