From 2b6361dcdbfc49fb7ae9e8b8d1ec43e9da92b610 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Tue, 6 Oct 2026 04:34:34 -0700 Subject: [PATCH 1/2] fix(home): REPL history and settings follow LUCLI_HOME ReplCommand, Terminal and Settings built their paths from user.home, so with LUCLI_HOME / -Dlucli.home set they still wrote into the real home, and Settings' settings.json wasn't the file 'system paths' reports. They now resolve the active LuCLI home through LucliPaths. Settings saved at the old location (user.home/.lucli/settings.json) are read once and copied to the active home when it has none; the old file is left untouched. Upstream: cybersonic/LuCLI#140 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Peter Amiri --- CHANGELOG.md | 2 + FORK.md | 1 + src/main/java/org/lucee/lucli/Settings.java | 20 +++++++++- src/main/java/org/lucee/lucli/Terminal.java | 6 +-- .../lucee/lucli/cli/commands/ReplCommand.java | 13 ++++-- .../java/org/lucee/lucli/SettingsTest.java | 40 +++++++++++++++++++ .../cli/commands/ReplCommandHistoryTest.java | 26 ++++++++++++ 7 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 src/test/java/org/lucee/lucli/cli/commands/ReplCommandHistoryTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index cb6c567d..d38adea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ All notable changes to this project will be documented in this file. ## Unreleased +- **Fix: REPL History and Settings Follow `LUCLI_HOME`:** `lucli repl` history, the interactive terminal's history and `Settings` (`settings.json`, used e.g. by `secrets provider use`) were written under `user.home` (`~/.lucli` or `~/`) even when `LUCLI_HOME` / `-Dlucli.home` pointed elsewhere, so an isolated LuCLI home still wrote into the real home, and `settings.json` was not the file `system paths` reports. They now use the active LuCLI home (`LucliPaths`). Settings saved under the old location (`~/.lucli/settings.json`) are read once and copied to the active home when it has none; the old file is left as it is. Upstream: cybersonic/LuCLI#140. Adds `SettingsTest` cases and `ReplCommandHistoryTest`. + - **Feature: Modules Can Tell an MCP `tools/call` From a Terminal Call:** During `tools/call`, the module function now receives a reserved `__lucliMcpCall=true` argument, so a module can apply a different policy to MCP clients (for example refusing a command meant for a person at the terminal). The argument is runtime-owned: any argument of that name (any case, with or without `--`) supplied by an MCP client or on the command line is removed before the module runs, so it can't be forged or suppressed, and a terminal invocation never carries it. The positional binder never fills a declared parameter of that name from a terminal `argN`. Documented in the MCP module servers guide. Adds coverage in `McpCommandTest` (`toolsCallCarriesARuntimeOwnedMcpMarker`, `terminalInvocationNeverCarriesTheMcpMarker`, `declaredMarkerParameterIsNeverBoundFromATerminalPositional`). - **Change: Dev Servers Listen on `127.0.0.1` by Default:** Servers used to listen on every network interface (Tomcat connectors had no `address`, Jetty no `jetty.http.host`, Docker published the port on all host interfaces), so a `lucli server start` was reachable from the whole local network. A new `lucee.json` key `bindAddress` (default `127.0.0.1`) now sets the HTTP and HTTPS connectors' `address` and that of every other connector (AJP, Nio2, APR, ...) that server.xml leaves without one, Jetty's `jetty.http.host`, the host side of Docker's `-p`, and JMX monitoring (`jmxremote.host` plus a matching RMI hostname and port). Unauthenticated JMX is refused on a non-loopback bind unless authentication is configured in `jvm.additionalArgs`, and the schema's documented `monitoring.enabled` default is corrected to `false`. Opt in to other interfaces with `"bindAddress": "0.0.0.0"`, `server start|run --host 0.0.0.0`, or the one-shot `bindAddress=0.0.0.0` override; the startup output shows the listen address and warns when it is all interfaces. `host` keeps its meaning (URL and certificate hostname). Adds coverage in `TomcatServerXmlPatcherTest`, `DockerRuntimeProviderTest`, `JettyBaseConfigGeneratorTest`, `LuceeServerConfigTest` and `ServerConfigHelperTest`. diff --git a/FORK.md b/FORK.md index 4336b501..2fec4a67 100644 --- a/FORK.md +++ b/FORK.md @@ -83,6 +83,7 @@ No release is tagged for this queue yet; tagging needs the Wheels release owner' | Order | Change | Purpose | |---|---|---| | 8 | wheels-dev/LuCLI [#19](https://github.com/wheels-dev/LuCLI/pull/19) (issue [#17](https://github.com/wheels-dev/LuCLI/issues/17)) | A module function run by an MCP `tools/call` receives the runtime-owned `__lucliMcpCall=true` argument; any caller-supplied argument of that name is dropped, and a terminal call never carries it | +| 10 | wheels-dev/LuCLI PR (this change; issue: upstream [cybersonic/LuCLI#140](https://github.com/cybersonic/LuCLI/issues/140)) | REPL / terminal history and `settings.json` follow the active LuCLI home instead of `user.home`; legacy `~/.lucli/settings.json` read once and copied | Upstream (row 8): issue [cybersonic/LuCLI#133](https://github.com/cybersonic/LuCLI/issues/133), PR [cybersonic/LuCLI#134](https://github.com/cybersonic/LuCLI/pull/134) (a `cherry-pick -x` of diff --git a/src/main/java/org/lucee/lucli/Settings.java b/src/main/java/org/lucee/lucli/Settings.java index e2107703..bcc3a924 100644 --- a/src/main/java/org/lucee/lucli/Settings.java +++ b/src/main/java/org/lucee/lucli/Settings.java @@ -1,5 +1,7 @@ package org.lucee.lucli; +import org.lucee.lucli.paths.LucliPaths; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -27,8 +29,10 @@ public class Settings { private JsonNode settings; public Settings() { - Path homeDir = Paths.get(System.getProperty("user.home")); - this.settingsDir = homeDir.resolve(SETTINGS_DIR); + // The active LuCLI home (-Dlucli.home, LUCLI_HOME, else ~/), the same + // file `system paths` reports. It used to be user.home/.lucli regardless of + // LUCLI_HOME (upstream cybersonic/LuCLI#140). + this.settingsDir = LucliPaths.resolve().home(); this.settingsFile = settingsDir.resolve(SETTINGS_FILE); this.promptsDir = settingsDir.resolve(PROMPTS_DIR); this.objectMapper = new ObjectMapper(); @@ -37,6 +41,11 @@ public Settings() { loadSettings(); } + /** Where settings were stored before they followed the active LuCLI home. */ + static Path legacySettingsFile() { + return Paths.get(System.getProperty("user.home"), SETTINGS_DIR, SETTINGS_FILE).toAbsolutePath().normalize(); + } + /** * Create necessary directories if they don't exist */ @@ -54,8 +63,15 @@ private void initializeDirectories() { */ private void loadSettings() { try { + Path legacyFile = legacySettingsFile(); if (Files.exists(settingsFile)) { settings = objectMapper.readTree(settingsFile.toFile()); + } else if (!legacyFile.equals(settingsFile) && Files.exists(legacyFile)) { + // Settings written before LuCLI honoured LUCLI_HOME / the profile home live in + // user.home/.lucli/settings.json. Read them once and save a copy to the active + // home; the legacy file is left as it is. + settings = objectMapper.readTree(legacyFile.toFile()); + saveSettings(); } else { // Create default settings settings = createDefaultSettings(); diff --git a/src/main/java/org/lucee/lucli/Terminal.java b/src/main/java/org/lucee/lucli/Terminal.java index 69ab5870..a3518b4f 100644 --- a/src/main/java/org/lucee/lucli/Terminal.java +++ b/src/main/java/org/lucee/lucli/Terminal.java @@ -138,9 +138,9 @@ private static void startInteractiveMode() throws Exception { return 0; // Don't exit terminal }); - // Set up history file - Path homeDir = Paths.get(System.getProperty("user.home")); - Path historyFile = homeDir.resolve(".lucli").resolve("history"); + // History lives in the active LuCLI home (it used to be user.home/.lucli + // regardless of LUCLI_HOME; upstream cybersonic/LuCLI#140). + Path historyFile = org.lucee.lucli.paths.LucliPaths.resolve().home().resolve("history"); LineReader reader = LineReaderBuilder.builder() .terminal(terminal) diff --git a/src/main/java/org/lucee/lucli/cli/commands/ReplCommand.java b/src/main/java/org/lucee/lucli/cli/commands/ReplCommand.java index 58e37a44..c9bc80fa 100644 --- a/src/main/java/org/lucee/lucli/cli/commands/ReplCommand.java +++ b/src/main/java/org/lucee/lucli/cli/commands/ReplCommand.java @@ -54,9 +54,7 @@ public Integer call() throws Exception { .name("CFML REPL") .build(); - // Set up history file using profile-aware home directory - Path homeDir = Paths.get(System.getProperty("user.home")); - Path historyFile = homeDir.resolve(LuCLI.getActiveProfile().homeDirName()).resolve("repl_history"); + Path historyFile = historyFile(); LineReader reader = LineReaderBuilder.builder() .terminal(terminal) @@ -162,4 +160,13 @@ private void printHelp(Terminal terminal) { terminal.writer().println(" structNew()"); terminal.writer().println(); } + + /** + * REPL history lives in the active LuCLI home (-Dlucli.home, LUCLI_HOME, else + * ~/). It used to be user.home/ regardless of + * LUCLI_HOME (upstream cybersonic/LuCLI#140). + */ + static Path historyFile() { + return org.lucee.lucli.paths.LucliPaths.resolve().home().resolve("repl_history"); + } } diff --git a/src/test/java/org/lucee/lucli/SettingsTest.java b/src/test/java/org/lucee/lucli/SettingsTest.java index 0a2b0936..1bc58b6f 100644 --- a/src/test/java/org/lucee/lucli/SettingsTest.java +++ b/src/test/java/org/lucee/lucli/SettingsTest.java @@ -37,6 +37,46 @@ void tearDown() { System.setProperty("user.home", originalUserHome); } + // ============================================ + // LuCLI home (upstream cybersonic/LuCLI#140) + // ============================================ + + @Test + void settingsFollowTheLucliHomeProperty() throws IOException { + Path custom = tempDir.resolve("custom-home"); + String previous = System.getProperty("lucli.home"); + System.setProperty("lucli.home", custom.toString()); + try { + Settings settings = new Settings(); + settings.setSelectedSecretProvider("probe-provider"); + assertEquals(custom, settings.getSettingsDir()); + assertTrue(Files.exists(custom.resolve("settings.json"))); + assertFalse(Files.exists(tempDir.resolve(".lucli").resolve("settings.json")), + "nothing may be written under user.home when lucli.home is set"); + } finally { + if (previous == null) System.clearProperty("lucli.home"); else System.setProperty("lucli.home", previous); + } + } + + @Test + void legacySettingsAreReadOnceAndCopiedToTheActiveHome() throws IOException { + Path legacyDir = tempDir.resolve(".lucli"); + Files.createDirectories(legacyDir); + String legacyJson = "{\"secretProvider\": \"legacy-provider\"}"; + Files.writeString(legacyDir.resolve("settings.json"), legacyJson); + Path custom = tempDir.resolve("custom-home"); + String previous = System.getProperty("lucli.home"); + System.setProperty("lucli.home", custom.toString()); + try { + Settings settings = new Settings(); + assertEquals("legacy-provider", settings.getString("secretProvider", "missing")); + assertTrue(Files.exists(custom.resolve("settings.json")), "copied to the active home"); + assertEquals(legacyJson, Files.readString(legacyDir.resolve("settings.json")), "legacy file left as it is"); + } finally { + if (previous == null) System.clearProperty("lucli.home"); else System.setProperty("lucli.home", previous); + } + } + // ============================================ // Initialization Tests // ============================================ diff --git a/src/test/java/org/lucee/lucli/cli/commands/ReplCommandHistoryTest.java b/src/test/java/org/lucee/lucli/cli/commands/ReplCommandHistoryTest.java new file mode 100644 index 00000000..4d64023c --- /dev/null +++ b/src/test/java/org/lucee/lucli/cli/commands/ReplCommandHistoryTest.java @@ -0,0 +1,26 @@ +package org.lucee.lucli.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** REPL history follows the active LuCLI home (upstream cybersonic/LuCLI#140). */ +class ReplCommandHistoryTest { + + @TempDir + Path tempDir; + + @Test + void historyFileIsInTheLucliHome() { + String previous = System.getProperty("lucli.home"); + System.setProperty("lucli.home", tempDir.toString()); + try { + assertEquals(tempDir.resolve("repl_history"), ReplCommand.historyFile()); + } finally { + if (previous == null) System.clearProperty("lucli.home"); else System.setProperty("lucli.home", previous); + } + } +} From 76cb0c468610e1d0c26df530337e41a4d5011e17 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Tue, 6 Oct 2026 04:35:00 -0700 Subject: [PATCH 2/2] docs(fork): link row 10 to #22 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Peter Amiri --- FORK.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FORK.md b/FORK.md index 2fec4a67..3ed90f08 100644 --- a/FORK.md +++ b/FORK.md @@ -83,7 +83,7 @@ No release is tagged for this queue yet; tagging needs the Wheels release owner' | Order | Change | Purpose | |---|---|---| | 8 | wheels-dev/LuCLI [#19](https://github.com/wheels-dev/LuCLI/pull/19) (issue [#17](https://github.com/wheels-dev/LuCLI/issues/17)) | A module function run by an MCP `tools/call` receives the runtime-owned `__lucliMcpCall=true` argument; any caller-supplied argument of that name is dropped, and a terminal call never carries it | -| 10 | wheels-dev/LuCLI PR (this change; issue: upstream [cybersonic/LuCLI#140](https://github.com/cybersonic/LuCLI/issues/140)) | REPL / terminal history and `settings.json` follow the active LuCLI home instead of `user.home`; legacy `~/.lucli/settings.json` read once and copied | +| 10 | wheels-dev/LuCLI [#22](https://github.com/wheels-dev/LuCLI/pull/22) (issue: upstream [cybersonic/LuCLI#140](https://github.com/cybersonic/LuCLI/issues/140)) | REPL / terminal history and `settings.json` follow the active LuCLI home instead of `user.home`; legacy `~/.lucli/settings.json` read once and copied | Upstream (row 8): issue [cybersonic/LuCLI#133](https://github.com/cybersonic/LuCLI/issues/133), PR [cybersonic/LuCLI#134](https://github.com/cybersonic/LuCLI/pull/134) (a `cherry-pick -x` of