diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b0e8e3..9ffa074 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`. + - **Fix: `--env` / `--envfile` Reach Modules Run Through the Module Shortcut:** `lucli … --env=x`, `lucli --env=x …` and an aliased binary (`-Dlucli.binary.name=`, e.g. `wheels start --env=prod`) re-execute `modules run …` on the root command, which reset the root's options, so the module saw `LuCLI.getCurrentEnvironment() == null` (only `LUCLI_ENV` worked). The shortcut now re-injects `--env` / `--envfile` at the root position, as it already did for `--verbose` / `--debug` (after the module args). Upstream: cybersonic/LuCLI#136. Adds `ModuleShortcutArgsTest`. - **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`). diff --git a/FORK.md b/FORK.md index bcb1f7f..109060b 100644 --- a/FORK.md +++ b/FORK.md @@ -84,6 +84,7 @@ No release is tagged for this queue yet; tagging needs the Wheels release owner' |---|---|---| | 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 | | 9 | wheels-dev/LuCLI [#21](https://github.com/wheels-dev/LuCLI/pull/21) (issue: upstream [cybersonic/LuCLI#136](https://github.com/cybersonic/LuCLI/issues/136)) | `--env` / `--envfile` reach a module run through the module shortcut or an aliased binary (re-injected at the root position) | +| 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 diff --git a/src/main/java/org/lucee/lucli/Settings.java b/src/main/java/org/lucee/lucli/Settings.java index e210770..bcc3a92 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 69ab587..a3518b4 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 58e37a4..c9bc80f 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 0a2b093..1bc58b6 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 0000000..4d64023 --- /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); + } + } +}