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: `--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`).

- **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`.
Expand Down
1 change: 1 addition & 0 deletions FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| 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) |

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
36 changes: 28 additions & 8 deletions src/main/java/org/lucee/lucli/LuCLI.java
Original file line number Diff line number Diff line change
Expand Up @@ -417,25 +417,45 @@ private Integer executeViaModulesCommand(String moduleName, String[] args) throw
verbose("Executing module shortcut: " + moduleName +
" (equivalent to 'lucli modules run " + moduleName + " " + String.join(" ", args) + "')");

List<String> cmdArgs = moduleShortcutArgs(moduleName, args, isVerbose(), isDebug(), envOption, envFileOption);
return spec.commandLine().execute(cmdArgs.toArray(new String[0]));
}

/**
* The argument list the module shortcut re-executes on the root command:
* {@code [--env=<env>] [--envfile=<path>] modules run <module> <args...> [--verbose] [--debug]}.
*
* Re-executing resets the root's options, so root-level flags the module must
* still see are re-injected. {@code --env} / {@code --envfile} go BEFORE
* {@code modules}, at the root position, where the execution strategy reads them
* into {@link #currentEnvironment} / {@link #envFilePath}; without them the
* module saw a null environment (upstream cybersonic/LuCLI#136).
* {@code --verbose} / {@code --debug} go after the module args, where the
* module's own arg parser reads them (e.g. the wheels module's
* {@code doctor --verbose}), and the subcommand stays the first positional.
*/
static List<String> moduleShortcutArgs(String moduleName, String[] args, boolean verbose, boolean debug,
String env, String envFile) {
List<String> cmdArgs = new ArrayList<>();
if (env != null && !env.isEmpty()) {
cmdArgs.add("--env=" + env);
}
if (envFile != null && !envFile.isEmpty()) {
cmdArgs.add("--envfile=" + envFile);
}
cmdArgs.add("modules");
cmdArgs.add("run");
cmdArgs.add(moduleName);
if (args != null && args.length > 0) {
cmdArgs.addAll(Arrays.asList(args));
}
// Re-inject root-level flags that picocli consumed at the root before the
// module shortcut dispatched, so the module can see them (e.g. the wheels
// module's `doctor --verbose` / `stats --verbose` / `test --verbose`).
// Appended AFTER the module args so the subcommand stays the first
// positional; the module's arg parser reads --verbose/--debug from there.
if (isVerbose()) {
if (verbose) {
cmdArgs.add("--verbose");
}
if (isDebug()) {
if (debug) {
cmdArgs.add("--debug");
}
return spec.commandLine().execute(cmdArgs.toArray(new String[0]));
return cmdArgs;
}

/**
Expand Down
47 changes: 47 additions & 0 deletions src/test/java/org/lucee/lucli/ModuleShortcutArgsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.lucee.lucli;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.List;

import org.junit.jupiter.api.Test;

/**
* The module shortcut ({@code lucli <module> ...} and an aliased binary) re-executes
* {@code modules run <module> ...} on the root command, which resets the root's
* options. Root flags the module must see are re-injected (cybersonic/LuCLI#136).
*/
class ModuleShortcutArgsTest {

@Test
void noRootFlagsGiveThePlainModulesRunArgs() {
assertEquals(
List.of("modules", "run", "wheels", "start", "--dry-run"),
LuCLI.moduleShortcutArgs("wheels", new String[] {"start", "--dry-run"}, false, false, null, null)
);
}

@Test
void envAndEnvFileAreReinjectedAtTheRootPosition() {
assertEquals(
List.of("--env=prod", "--envfile=/tmp/x.env", "modules", "run", "wheels", "start"),
LuCLI.moduleShortcutArgs("wheels", new String[] {"start"}, false, false, "prod", "/tmp/x.env")
);
}

@Test
void verboseAndDebugStayAfterTheModuleArgs() {
assertEquals(
List.of("--env=staging", "modules", "run", "wheels", "doctor", "--verbose", "--debug"),
LuCLI.moduleShortcutArgs("wheels", new String[] {"doctor"}, true, true, "staging", "")
);
}

@Test
void emptyEnvIsNotReinjected() {
assertEquals(
List.of("modules", "run", "mod"),
LuCLI.moduleShortcutArgs("mod", new String[0], false, false, "", null)
);
}
}
Loading