Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `~/<profile home>`) 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 <module> … --env=x`, `lucli --env=x <module> …` and an aliased binary (`-Dlucli.binary.name=<module>`, 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`).
Expand Down
1 change: 1 addition & 0 deletions FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 18 additions & 2 deletions src/main/java/org/lucee/lucli/Settings.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 ~/<profile home>), 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();
Expand All @@ -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
*/
Expand All @@ -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();
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/org/lucee/lucli/Terminal.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 10 additions & 3 deletions src/main/java/org/lucee/lucli/cli/commands/ReplCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
* ~/<profile home>). It used to be user.home/<profile home> regardless of
* LUCLI_HOME (upstream cybersonic/LuCLI#140).
*/
static Path historyFile() {
return org.lucee.lucli.paths.LucliPaths.resolve().home().resolve("repl_history");
}
}
40 changes: 40 additions & 0 deletions src/test/java/org/lucee/lucli/SettingsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ============================================
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Loading