This is an automated email from the ASF dual-hosted git repository.

jamesfredley pushed a commit to branch issue-13752-upgrade-jansi-jline
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 958bfea6642a4ff0af1a1c5b3fc39c73b30a3921
Author: James Fredley <[email protected]>
AuthorDate: Fri Jan 30 18:57:20 2026 -0500

    Upgrade JLine to 3.27.1 and Jansi to 2.4.1
    
    Closes #13752
    
    - Update jline from 2.14.6 to 3.27.1 (org.jline:jline)
    - Update jansi from 1.18 to 2.4.1
    - Migrate all CLI code from JLine 2 API to JLine 3 API:
      - ConsoleReader -> LineReader/Terminal
      - jline.console.completer.Completer -> org.jline.reader.Completer
      - complete(buffer, cursor, candidates) -> complete(reader, line, 
candidates)
      - CharSequence candidates -> Candidate objects
    - Keep JLine 2 (jline:jline:2.14.6) for groovy-groovysh compatibility
      (Groovy 4.x groovysh requires JLine 2; Groovy 5.x uses JLine 3)
    - Add TODO comments for JLine 2 removal when upgrading to Groovy 5
    - Add JLine 3.27.1 license mapping in SbomPlugin (BSD-3-Clause)
    - Update RegexCompletorSpec tests for JLine 3 API
---
 .../org/apache/grails/buildsrc/SbomPlugin.groovy   |   3 +-
 dependencies.gradle                                |  10 +-
 gradle/docs-dependencies.gradle                    |   1 +
 grails-bootstrap/build.gradle                      |   4 +-
 .../groovy/grails/build/logging/GrailsConsole.java | 156 ++++++++++-----------
 .../grails/build/logging/GrailsEclipseConsole.java |  12 +-
 .../CandidateListCompletionHandler.java            |  70 ++++-----
 .../grails/build/logging/GrailsConsoleSpec.groovy  |  18 ++-
 grails-console/build.gradle                        |   2 +
 grails-controllers/build.gradle                    |   2 +-
 grails-forge/gradle.properties                     |   2 +-
 grails-gradle/gradle/docs-config.gradle            |   1 +
 grails-gradle/model/build.gradle                   |   3 +-
 .../grails/cli/profile/CommandDescription.groovy   |   2 +-
 grails-scaffolding/build.gradle                    |   3 +-
 grails-shell-cli/build.gradle                      |   2 +-
 .../main/groovy/org/grails/cli/GrailsCli.groovy    |  84 ++++++-----
 .../cli/gradle/commands/GradleCommand.groovy       |  14 +-
 .../interactive/completers/ClosureCompleter.groovy |  11 +-
 .../completers/EscapingFileNameCompletor.groovy    |  26 ++--
 .../interactive/completers/RegexCompletor.groovy   |  20 +--
 .../completers/SimpleOrFileNameCompletor.groovy    |  29 ++--
 .../completers/SortedAggregateCompleter.java       |  78 +++--------
 .../interactive/completers/StringsCompleter.java   |  37 ++---
 .../org/grails/cli/profile/AbstractProfile.groovy  |   4 +-
 .../groovy/org/grails/cli/profile/Profile.java     |   4 +-
 .../commands/ArgumentCompletingCommand.groovy      |  20 +--
 .../cli/profile/commands/CommandCompleter.groovy   |  11 +-
 .../cli/profile/commands/CreateAppCommand.groovy   |  26 ++--
 .../commands/DefaultMultiStepCommand.groovy        |   2 +-
 .../grails/cli/profile/commands/HelpCommand.groovy |  13 +-
 .../grails/cli/profile/commands/OpenCommand.groovy |  11 +-
 .../test/groovy/org/grails/cli/TestTerminal.java   |  29 +++-
 .../completers/RegexCompletorSpec.groovy           |  26 ++--
 grails-test-core/build.gradle                      |   2 +-
 grails-web-url-mappings/build.gradle               |   2 +-
 36 files changed, 381 insertions(+), 359 deletions(-)

diff --git 
a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy
 
b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy
index 6f3d7b9eea..01b7cdafa6 100644
--- 
a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy
+++ 
b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy
@@ -88,7 +88,8 @@ class SbomPlugin implements Plugin<Project> {
     private static Map<String, String> LICENSE_MAPPING = [
             'pkg:maven/org.antlr/[email protected]?type=jar'               
: 'BSD-3-Clause', // maps incorrectly because of 
https://github.com/CycloneDX/cyclonedx-core-java/issues/205
             'pkg:maven/jline/[email protected]?type=jar'                           
: 'BSD-2-Clause', // maps incorrectly because of 
https://github.com/CycloneDX/cyclonedx-core-java/issues/205
-            'pkg:maven/org.jline/[email protected]?type=jar'                       
: 'BSD-2-Clause', // maps incorrectly because of 
https://github.com/CycloneDX/cyclonedx-core-java/issues/205
+            'pkg:maven/org.jline/[email protected]?type=jar'                       
: 'BSD-3-Clause', // maps incorrectly because of 
https://github.com/CycloneDX/cyclonedx-core-java/issues/205
+            'pkg:maven/org.jline/[email protected]?type=jar'                       
: 'BSD-3-Clause', // maps incorrectly because of 
https://github.com/CycloneDX/cyclonedx-core-java/issues/205
             
'pkg:maven/org.liquibase.ext/[email protected]?type=jar': 
'Apache-2.0', // maps incorrectly because of 
https://github.com/liquibase/liquibase/issues/2445 & the base pom does not 
define a license
             
'pkg:maven/com.oracle.coherence.ce/[email protected]?type=pom': 'UPL-1.0', 
// does not have map based on license id
             
'pkg:maven/com.oracle.coherence.ce/[email protected]?type=pom': 'UPL-1.0', 
// does not have map based on license id
diff --git a/dependencies.gradle b/dependencies.gradle
index c87f4af529..c7ea63654a 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -31,9 +31,11 @@ ext {
             'directory-watcher.version'     : '0.19.1',
             'gradle-spock.version'          : '2.3-groovy-3.0',
             'grails-publish-plugin.version' : '0.0.4',
-            'jansi.version'                 : '1.18',
+            'jansi.version'                 : '2.4.1',
             'javaparser-core.version'       : '3.27.0',
-            'jline.version'                 : '2.14.6',
+            'jline.version'                 : '3.27.1',
+            // TODO: Remove jline2 when upgrading to Groovy 5 (groovy-groovysh 
5.x uses JLine 3)
+            'jline2.version'                : '2.14.6',
             'jna.version'                   : '5.17.0',
             'jquery.version'                : '3.7.1',
             'objenesis.version'             : '3.4',
@@ -59,7 +61,9 @@ ext {
             'grails-publish-plugin' : 
"org.apache.grails.gradle:grails-publish:${gradleBomDependencyVersions['grails-publish-plugin.version']}",
             'jansi'                 : 
"org.fusesource.jansi:jansi:${gradleBomDependencyVersions['jansi.version']}",
             'javaparser-core'       : 
"com.github.javaparser:javaparser-core:${gradleBomDependencyVersions['javaparser-core.version']}",
-            'jline'                 : 
"jline:jline:${gradleBomDependencyVersions['jline.version']}",
+            'jline'                 : 
"org.jline:jline:${gradleBomDependencyVersions['jline.version']}",
+            // TODO: Remove jline2 when upgrading to Groovy 5 (groovy-groovysh 
5.x uses JLine 3)
+            'jline2'                : 
"jline:jline:${gradleBomDependencyVersions['jline2.version']}",
             'jna'                   : 
"net.java.dev.jna:jna:${gradleBomDependencyVersions['jna.version']}",
             'objenesis'             : 
"org.objenesis:objenesis:${gradleBomDependencyVersions['objenesis.version']}",
             'spring-boot-cli'       : 
"org.springframework.boot:spring-boot-cli:${gradleBomDependencyVersions['spring-boot.version']}",
diff --git a/gradle/docs-dependencies.gradle b/gradle/docs-dependencies.gradle
index df4244f39c..828aaa7dc0 100644
--- a/gradle/docs-dependencies.gradle
+++ b/gradle/docs-dependencies.gradle
@@ -29,6 +29,7 @@ configurations.register('documentation') {
 dependencies {
     add('documentation', platform(project(':grails-bom')))
     add('documentation', 'org.fusesource.jansi:jansi')
+    // TODO: Remove jline:jline (JLine 2) when upgrading to Groovy 5 
(groovy-groovysh 5.x uses JLine 3)
     add('documentation', 'jline:jline')
     add('documentation', 'com.github.javaparser:javaparser-core')
     add('documentation', 'org.apache.groovy:groovy')
diff --git a/grails-bootstrap/build.gradle b/grails-bootstrap/build.gradle
index 22bf226e4f..934cda7785 100644
--- a/grails-bootstrap/build.gradle
+++ b/grails-bootstrap/build.gradle
@@ -50,7 +50,7 @@ dependencies {
 
     compileOnly 'io.methvin:directory-watcher'
     compileOnly 'org.fusesource.jansi:jansi'
-    compileOnly 'jline:jline'
+    compileOnly 'org.jline:jline'
     compileOnly 'net.java.dev.jna:jna'
 
     api 'org.yaml:snakeyaml'
@@ -58,7 +58,7 @@ dependencies {
     testImplementation 'org.apache.groovy:groovy-xml'
     testImplementation 'org.apache.groovy:groovy-templates'
     testImplementation 'org.fusesource.jansi:jansi'
-    testImplementation 'jline:jline'
+    testImplementation 'org.jline:jline'
 
     testImplementation 'org.apache.groovy:groovy-test-junit5'
     testImplementation 'org.junit.jupiter:junit-jupiter-api'
diff --git 
a/grails-bootstrap/src/main/groovy/grails/build/logging/GrailsConsole.java 
b/grails-bootstrap/src/main/groovy/grails/build/logging/GrailsConsole.java
index 986bf64083..dea79be276 100644
--- a/grails-bootstrap/src/main/groovy/grails/build/logging/GrailsConsole.java
+++ b/grails-bootstrap/src/main/groovy/grails/build/logging/GrailsConsole.java
@@ -18,14 +18,13 @@
  */
 package grails.build.logging;
 
-import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.io.Flushable;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintStream;
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.nio.file.Path;
 import java.util.Collection;
 import java.util.List;
 import java.util.Stack;
@@ -34,23 +33,21 @@ import org.codehaus.groovy.runtime.DefaultGroovyMethods;
 import org.codehaus.groovy.runtime.StackTraceUtils;
 import org.codehaus.groovy.runtime.typehandling.NumberMath;
 
-import jline.Terminal;
-import jline.TerminalFactory;
-import jline.UnixTerminal;
-import jline.console.ConsoleReader;
-import jline.console.completer.Completer;
-import jline.console.history.FileHistory;
-import jline.console.history.History;
-import jline.internal.Log;
-import jline.internal.ShutdownHooks;
-import jline.internal.TerminalLineSettings;
+import org.jline.reader.Completer;
+import org.jline.reader.History;
+import org.jline.reader.LineReader;
+import org.jline.reader.LineReaderBuilder;
+import org.jline.reader.impl.history.DefaultHistory;
+import org.jline.terminal.Terminal;
+import org.jline.terminal.TerminalBuilder;
+import org.jline.utils.AttributedString;
+import org.jline.utils.AttributedStyle;
 import org.apache.tools.ant.BuildException;
 import org.fusesource.jansi.Ansi;
 import org.fusesource.jansi.Ansi.Color;
 import org.fusesource.jansi.AnsiConsole;
 
 import grails.util.Environment;
-import org.grails.build.interactive.CandidateListCompletionHandler;
 import org.grails.build.logging.GrailsConsoleErrorPrintStream;
 import org.grails.build.logging.GrailsConsolePrintStream;
 
@@ -114,7 +111,7 @@ public class GrailsConsole implements ConsoleLogger {
     /**
      * The reader to read info from the console
      */
-    ConsoleReader reader;
+    LineReader reader;
 
     Terminal terminal;
 
@@ -179,8 +176,8 @@ public class GrailsConsole implements ConsoleLogger {
      * @throws IOException
      */
     public void reinitialize(InputStream systemIn, PrintStream systemOut, 
PrintStream systemErr) throws IOException {
-        if (reader != null) {
-            reader.shutdown();
+        if (terminal != null) {
+            terminal.close();
         }
         initialize(systemIn, systemOut, systemErr);
     }
@@ -190,19 +187,12 @@ public class GrailsConsole implements ConsoleLogger {
 
         redirectSystemOutAndErr(true);
 
-        System.setProperty(ShutdownHooks.JLINE_SHUTDOWNHOOK, "false");
-
         if (isInteractiveEnabled()) {
-            reader = createConsoleReader(systemIn);
-            reader.setBellEnabled(false);
-            reader.setCompletionHandler(new CandidateListCompletionHandler());
-            if (isActivateTerminal()) {
-                terminal = createTerminal();
-            }
-
+            terminal = createTerminal();
+            reader = createLineReader(terminal);
             history = prepareHistory();
             if (history != null) {
-                reader.setHistory(history);
+                reader.setVariable(LineReader.HISTORY_FILE, new 
File(System.getProperty("user.home"), HISTORYFILE).toPath());
             }
         } else if (isActivateTerminal()) {
             terminal = createTerminal();
@@ -251,51 +241,50 @@ public class GrailsConsole implements ConsoleLogger {
         return property == null ? true : Boolean.valueOf(property);
     }
 
-    protected ConsoleReader createConsoleReader(InputStream systemIn) throws 
IOException {
-        // need to swap out the output to avoid logging during init
-        final PrintStream nullOutput = new PrintStream(new 
ByteArrayOutputStream());
-        final PrintStream originalOut = Log.getOutput();
-        try {
-            Log.setOutput(nullOutput);
-            ConsoleReader consoleReader = new ConsoleReader(systemIn, out);
-            consoleReader.setExpandEvents(false);
-            return consoleReader;
-        } finally {
-            Log.setOutput(originalOut);
-        }
+    protected LineReader createLineReader(Terminal terminal) throws 
IOException {
+        LineReader lineReader = LineReaderBuilder.builder()
+                .terminal(terminal)
+                .option(LineReader.Option.DISABLE_EVENT_EXPANSION, true)
+                .build();
+        return lineReader;
     }
 
     /**
-     * Creates the instance of Terminal used directly in GrailsConsole. Note 
that there is also
-     * another terminal instance created implicitly inside of ConsoleReader. 
That instance
-     * is controlled by the jline.terminal system property.
+     * Creates the instance of Terminal used directly in GrailsConsole.
      */
-    protected Terminal createTerminal() {
-        terminal = TerminalFactory.create();
-        if (isWindows()) {
-            terminal.setEchoEnabled(true);
-        }
+    protected Terminal createTerminal() throws IOException {
+        Terminal terminal = TerminalBuilder.builder()
+                .system(true)
+                .build();
         return terminal;
     }
 
     public void resetCompleters() {
-        final ConsoleReader reader = getReader();
-        if (reader != null) {
-            Collection<Completer> completers = reader.getCompleters();
-            for (Completer completer : completers) {
-                reader.removeCompleter(completer);
-            }
+        // In JLine 3, completers are set at LineReader creation time or via 
setCompleter
+        // We'll handle this differently - completers are managed via the 
LineReader
+    }
 
-            // for some unknown reason / bug in JLine you have to iterate over 
twice to clear the completers (WTF)
-            completers = reader.getCompleters();
-            for (Completer completer : completers) {
-                reader.removeCompleter(completer);
+    public void addCompleter(Completer completer) {
+        // In JLine 3, we need to recreate the LineReader with the new 
completer
+        // or use an AggregateCompleter. For now, this is a simplified 
implementation.
+        if (terminal != null) {
+            try {
+                reader = LineReaderBuilder.builder()
+                        .terminal(terminal)
+                        .completer(completer)
+                        .option(LineReader.Option.DISABLE_EVENT_EXPANSION, 
true)
+                        .build();
+                if (history != null) {
+                    reader.setVariable(LineReader.HISTORY_FILE, new 
File(System.getProperty("user.home"), HISTORYFILE).toPath());
+                }
+            } catch (Exception e) {
+                // ignore
             }
         }
     }
 
     /**
-     * Prepares a history file to be used by the ConsoleReader. This file
+     * Prepares a history file to be used by the LineReader. This file
      * will live in the home directory of the user.
      */
     protected History prepareHistory() throws IOException {
@@ -307,7 +296,7 @@ public class GrailsConsole implements ConsoleLogger {
                 // can't create the file, so no history for you
             }
         }
-        return file.canWrite() ? new FileHistory(file) : null;
+        return file.canWrite() ? new DefaultHistory() : null;
     }
 
     public boolean isWindows() {
@@ -334,8 +323,12 @@ public class GrailsConsole implements ConsoleLogger {
         if (instance != null) {
             instance.removeShutdownHook();
             instance.restoreOriginalSystemOutAndErr();
-            if (instance.getReader() != null) {
-                instance.getReader().shutdown();
+            if (instance.terminal != null) {
+                try {
+                    instance.terminal.close();
+                } catch (IOException e) {
+                    // ignore
+                }
             }
             instance = null;
         }
@@ -348,24 +341,18 @@ public class GrailsConsole implements ConsoleLogger {
 
     protected void restoreTerminal() {
         try {
-            terminal.restore();
+            if (terminal != null) {
+                terminal.close();
+            }
         } catch (Exception e) {
             // ignore
         }
-        if (terminal instanceof UnixTerminal) {
-            // workaround for GRAILS-11494
-            try {
-                new TerminalLineSettings().set("sane");
-            } catch (Exception e) {
-                // ignore
-            }
-        }
     }
 
     protected void persistHistory() {
-        if (history instanceof Flushable) {
+        if (history != null && reader != null) {
             try {
-                ((Flushable) history).flush();
+                history.save();
             } catch (Throwable e) {
                 // ignore exception
             }
@@ -442,7 +429,7 @@ public class GrailsConsole implements ConsoleLogger {
      */
     public InputStream getInput() {
         assertAllowInput();
-        return reader.getInput();
+        return terminal != null ? terminal.input() : System.in;
     }
 
     private void assertAllowInput() {
@@ -471,7 +458,7 @@ public class GrailsConsole implements ConsoleLogger {
         this.lastMessage = lastMessage;
     }
 
-    public ConsoleReader getReader() {
+    public LineReader getReader() {
         return reader;
     }
 
@@ -674,7 +661,7 @@ public class GrailsConsole implements ConsoleLogger {
     }
 
     public boolean isAnsiEnabled() {
-        return Ansi.isEnabled() && (terminal != null && 
terminal.isAnsiSupported()) && ansiEnabled;
+        return Ansi.isEnabled() && (terminal != null && terminal.getType() != 
Terminal.TYPE_DUMB) && ansiEnabled;
     }
 
     /**
@@ -875,10 +862,15 @@ public class GrailsConsole implements ConsoleLogger {
         assertAllowInput(prompt);
         userInputActive = true;
         try {
-            Character inputMask = secure ? SECURE_MASK_CHAR : defaultInputMask;
-            return reader.readLine(prompt, inputMask);
-        } catch (IOException e) {
-            throw new RuntimeException("Error reading input: " + 
e.getMessage());
+            if (secure) {
+                return reader.readLine(prompt, SECURE_MASK_CHAR);
+            } else {
+                return reader.readLine(prompt, defaultInputMask, (String) 
null);
+            }
+        } catch (org.jline.reader.UserInterruptException e) {
+            return null;
+        } catch (org.jline.reader.EndOfFileException e) {
+            return null;
         } finally {
             userInputActive = false;
         }
@@ -1041,4 +1033,12 @@ public class GrailsConsole implements ConsoleLogger {
     public void setDefaultInputMask(Character defaultInputMask) {
         this.defaultInputMask = defaultInputMask;
     }
+
+    /**
+     * Gets the history for the LineReader
+     * @return the history
+     */
+    public History getHistory() {
+        return history;
+    }
 }
diff --git 
a/grails-bootstrap/src/main/groovy/grails/build/logging/GrailsEclipseConsole.java
 
b/grails-bootstrap/src/main/groovy/grails/build/logging/GrailsEclipseConsole.java
index a9142afb55..d2fe51eeb1 100644
--- 
a/grails-bootstrap/src/main/groovy/grails/build/logging/GrailsEclipseConsole.java
+++ 
b/grails-bootstrap/src/main/groovy/grails/build/logging/GrailsEclipseConsole.java
@@ -20,8 +20,8 @@ package grails.build.logging;
 
 import java.io.IOException;
 
-import jline.Terminal;
-import jline.UnsupportedTerminal;
+import org.jline.terminal.Terminal;
+import org.jline.terminal.TerminalBuilder;
 
 /**
  * This class is meant to keep changes made in support of Eclipse separate from
@@ -73,8 +73,10 @@ public class GrailsEclipseConsole extends GrailsConsole {
     }
 
     @Override
-    protected Terminal createTerminal() {
-        // unix or windows terminal have no relation at all to the behavior of 
an Eclipse console.
-        return new UnsupportedTerminal();
+    protected Terminal createTerminal() throws IOException {
+        // For Eclipse, create a dumb terminal that doesn't try to interact 
with the console
+        return TerminalBuilder.builder()
+                .dumb(true)
+                .build();
     }
 }
diff --git 
a/grails-bootstrap/src/main/groovy/org/grails/build/interactive/CandidateListCompletionHandler.java
 
b/grails-bootstrap/src/main/groovy/org/grails/build/interactive/CandidateListCompletionHandler.java
index 2b9ff8abb4..564ac5a5cc 100644
--- 
a/grails-bootstrap/src/main/groovy/org/grails/build/interactive/CandidateListCompletionHandler.java
+++ 
b/grails-bootstrap/src/main/groovy/org/grails/build/interactive/CandidateListCompletionHandler.java
@@ -18,76 +18,62 @@
  */
 package org.grails.build.interactive;
 
-import java.io.IOException;
 import java.util.List;
 
-import jline.console.ConsoleReader;
-import jline.console.CursorBuffer;
-import jline.console.completer.CompletionHandler;
+import org.jline.reader.Candidate;
+import org.jline.reader.Completer;
+import org.jline.reader.LineReader;
+import org.jline.reader.ParsedLine;
 
 /**
- * Fixes issues with the default CandidateListCompletionHandler such as 
clearing out the whole buffer when
- * a completion matches a list of candidates
+ * A Completer implementation that wraps candidate list completion behavior.
+ * In JLine 3, completion handling is integrated into the LineReader itself,
+ * so this class now acts as a utility completer that can be composed with 
others.
  *
  * @author Graeme Rocher
  * @since 2.0
  */
-public class CandidateListCompletionHandler implements CompletionHandler {
+public class CandidateListCompletionHandler implements Completer {
 
+    private final Completer delegate;
     private boolean eagerNewlines = true;
 
-    public void setAlwaysIncludeNewline(boolean eagerNewlines) {
-        this.eagerNewlines = eagerNewlines;
+    public CandidateListCompletionHandler() {
+        this.delegate = null;
     }
 
-    public boolean complete(ConsoleReader reader, 
@SuppressWarnings("rawtypes") List<CharSequence> candidates, int pos) throws 
IOException {
-        CursorBuffer buf = reader.getCursorBuffer();
-
-        // if there is only one completion, then fill in the buffer
-        if (candidates.size() == 1) {
-            String value = candidates.get(0).toString();
-
-            // fail if the only candidate is the same as the current buffer
-            if (value.equals(buf.toString())) {
-                return false;
-            }
-
-            
jline.console.completer.CandidateListCompletionHandler.setBuffer(reader, value, 
pos);
-
-            return true;
-        }
-
-        if (candidates.size() > 1) {
-            String value = getUnambiguousCompletions(candidates);
+    public CandidateListCompletionHandler(Completer delegate) {
+        this.delegate = delegate;
+    }
 
-            
jline.console.completer.CandidateListCompletionHandler.setBuffer(reader, value, 
pos);
-        }
+    public void setAlwaysIncludeNewline(boolean eagerNewlines) {
+        this.eagerNewlines = eagerNewlines;
+    }
 
-        if (eagerNewlines) {
-            reader.println();
+    @Override
+    public void complete(LineReader reader, ParsedLine line, List<Candidate> 
candidates) {
+        if (delegate != null) {
+            delegate.complete(reader, line, candidates);
         }
-        
jline.console.completer.CandidateListCompletionHandler.printCandidates(reader, 
candidates);
-
-        // redraw the current console buffer
-        reader.drawLine();
-
-        return true;
     }
 
     /**
      * Returns a root that matches all the {@link String} elements
      * of the specified {@link List}, or null if there are
-     * no commalities. For example, if the list contains
+     * no commonalities. For example, if the list contains
      * <i>foobar</i>, <i>foobaz</i>, <i>foobuz</i>, the
      * method will return <i>foob</i>.
      */
-    private final String getUnambiguousCompletions(final List<?> candidates) {
+    public static String getUnambiguousCompletions(final List<Candidate> 
candidates) {
         if (candidates == null || candidates.isEmpty()) {
             return null;
         }
 
         // convert to an array for speed
-        String[] strings = candidates.toArray(new String[candidates.size()]);
+        String[] strings = new String[candidates.size()];
+        for (int i = 0; i < candidates.size(); i++) {
+            strings[i] = candidates.get(i).value();
+        }
 
         String first = strings[0];
         StringBuilder candidate = new StringBuilder();
@@ -107,7 +93,7 @@ public class CandidateListCompletionHandler implements 
CompletionHandler {
      * @return true is all the elements of <i>candidates</i>
      *         start with <i>starts</i>
      */
-    private final boolean startsWith(final String starts, final String[] 
candidates) {
+    private static boolean startsWith(final String starts, final String[] 
candidates) {
         for (int i = 0; i < candidates.length; i++) {
             if (!candidates[i].startsWith(starts)) {
                 return false;
diff --git 
a/grails-bootstrap/src/test/groovy/grails/build/logging/GrailsConsoleSpec.groovy
 
b/grails-bootstrap/src/test/groovy/grails/build/logging/GrailsConsoleSpec.groovy
index dd970e5479..4f99dd9650 100644
--- 
a/grails-bootstrap/src/test/groovy/grails/build/logging/GrailsConsoleSpec.groovy
+++ 
b/grails-bootstrap/src/test/groovy/grails/build/logging/GrailsConsoleSpec.groovy
@@ -18,7 +18,10 @@
  */
 package grails.build.logging
 
-import jline.console.ConsoleReader
+import org.jline.reader.LineReader
+import org.jline.reader.LineReaderBuilder
+import org.jline.terminal.Terminal
+import org.jline.terminal.TerminalBuilder
 import org.fusesource.jansi.Ansi
 import spock.lang.IgnoreIf
 import spock.lang.Issue
@@ -48,20 +51,27 @@ class GrailsConsoleSpec extends Specification {
     PrintStream out
     GrailsConsole console
     String output
+    Terminal terminal
 
     def setup() {
-        InputStream systemIn = Mock(InputStream)
-        systemIn.read(* _) >> -1
         out = Mock(PrintStream)
+        terminal = TerminalBuilder.builder().dumb(true).build()
 
         console = GrailsConsole.getInstance()
         console.ansiEnabled = true
         console.out = out
-        console.reader = new ConsoleReader(systemIn, out)
+        console.@terminal = terminal
+        console.@reader = LineReaderBuilder.builder()
+                .terminal(terminal)
+                .build()
 
         output = ""
     }
 
+    def cleanup() {
+        terminal?.close()
+    }
+
     @Issue('GRAILS-10753')
     def "outputMessage - verify the reset marker at the end of the output"() {
         when:
diff --git a/grails-console/build.gradle b/grails-console/build.gradle
index 74778ffc97..0308475832 100644
--- a/grails-console/build.gradle
+++ b/grails-console/build.gradle
@@ -41,6 +41,8 @@ dependencies {
     api 'org.apache.groovy:groovy-swing'
     api 'org.apache.groovy:groovy-groovysh'
     implementation 'org.fusesource.jansi:jansi'
+    implementation 'org.jline:jline'
+    // TODO: Remove jline:jline (JLine 2) when upgrading to Groovy 5 
(groovy-groovysh 5.x uses JLine 3)
     implementation 'jline:jline'
     implementation 'net.java.dev.jna:jna'
 
diff --git a/grails-controllers/build.gradle b/grails-controllers/build.gradle
index 916444c72d..404ebab570 100644
--- a/grails-controllers/build.gradle
+++ b/grails-controllers/build.gradle
@@ -47,7 +47,7 @@ dependencies {
 
     runtimeOnly project(':grails-i18n')
 
-    testRuntimeOnly 'jline:jline'
+    testRuntimeOnly 'org.jline:jline'
     testRuntimeOnly 'org.fusesource.jansi:jansi'
 
     compileOnly 'jakarta.servlet:jakarta.servlet-api'
diff --git a/grails-forge/gradle.properties b/grails-forge/gradle.properties
index e0adeeda0b..4b04b11a4f 100644
--- a/grails-forge/gradle.properties
+++ b/grails-forge/gradle.properties
@@ -35,7 +35,7 @@ groovyVersion=3.0.25
 jacksonDatabindVersion=2.18.3
 jakartaInjectVersion=1.0.5
 # match the jansi version in grails-bom
-jansiVersion=1.18
+jansiVersion=2.4.1
 javaDiffUtils=4.15
 jgitVersion=6.10.0.202406032230-r
 logbackClassicVersion=1.5.17
diff --git a/grails-gradle/gradle/docs-config.gradle 
b/grails-gradle/gradle/docs-config.gradle
index be2aaf4f1c..685a00fd5d 100644
--- a/grails-gradle/gradle/docs-config.gradle
+++ b/grails-gradle/gradle/docs-config.gradle
@@ -29,6 +29,7 @@ configurations.register('documentation') {
 dependencies {
     add('documentation', platform(project(':grails-gradle-bom')))
     add('documentation', 'org.fusesource.jansi:jansi')
+    // TODO: Remove jline:jline (JLine 2) when upgrading to Groovy 5 
(groovy-groovysh 5.x uses JLine 3)
     add('documentation', 'jline:jline')
     add('documentation', 'com.github.javaparser:javaparser-core')
     add('documentation', 
"org.apache.groovy:groovy:${bomDependencyVersions['groovy.version']}")
diff --git a/grails-gradle/model/build.gradle b/grails-gradle/model/build.gradle
index 7df57487ca..13203d7e3d 100644
--- a/grails-gradle/model/build.gradle
+++ b/grails-gradle/model/build.gradle
@@ -58,7 +58,8 @@ dependencies {
         // impl: org.springframework.boot.env.YamlPropertySourceLoader
     }
 
-    compileOnly 'jline:jline' // for profile compilation
+    compileOnly 'org.jline:jline' // for profile compilation
+    compileOnly 'org.fusesource.jansi:jansi'
 
     api 'org.yaml:snakeyaml'
 
diff --git 
a/grails-gradle/model/src/main/groovy/org/grails/cli/profile/CommandDescription.groovy
 
b/grails-gradle/model/src/main/groovy/org/grails/cli/profile/CommandDescription.groovy
index ddef7afb50..bf56218e0c 100644
--- 
a/grails-gradle/model/src/main/groovy/org/grails/cli/profile/CommandDescription.groovy
+++ 
b/grails-gradle/model/src/main/groovy/org/grails/cli/profile/CommandDescription.groovy
@@ -22,7 +22,7 @@ import groovy.transform.Canonical
 import groovy.transform.CompileDynamic
 import groovy.transform.CompileStatic
 
-import jline.console.completer.Completer
+import org.jline.reader.Completer
 
 /**
  * Describes a {@link Command}
diff --git a/grails-scaffolding/build.gradle b/grails-scaffolding/build.gradle
index eb76fa3d09..ec73e3d58f 100644
--- a/grails-scaffolding/build.gradle
+++ b/grails-scaffolding/build.gradle
@@ -41,7 +41,8 @@ dependencies {
     api project(':grails-fields')
     api project(':grails-rest-transforms')
 
-    compileOnly 'jline:jline'
+    compileOnly 'org.jline:jline'
+    compileOnly 'org.fusesource.jansi:jansi'
 
     testImplementation 'org.spockframework:spock-core'
     testImplementation project(':grails-web-gsp')
diff --git a/grails-shell-cli/build.gradle b/grails-shell-cli/build.gradle
index a66c828e65..894c55380e 100644
--- a/grails-shell-cli/build.gradle
+++ b/grails-shell-cli/build.gradle
@@ -54,7 +54,7 @@ dependencies {
     api 'org.apache.grails.gradle:grails-gradle-model'
     api 'org.apache.ant:ant'
     api 'org.fusesource.jansi:jansi'
-    api 'jline:jline'
+    api 'org.jline:jline'
     api "org.gradle:gradle-tooling-api:$gradleToolingApiVersion"
     
     compileOnly 'org.springframework.boot:spring-boot'
diff --git a/grails-shell-cli/src/main/groovy/org/grails/cli/GrailsCli.groovy 
b/grails-shell-cli/src/main/groovy/org/grails/cli/GrailsCli.groovy
index 0cd30756e2..da19b47e46 100644
--- a/grails-shell-cli/src/main/groovy/org/grails/cli/GrailsCli.groovy
+++ b/grails-shell-cli/src/main/groovy/org/grails/cli/GrailsCli.groovy
@@ -25,11 +25,14 @@ import java.util.concurrent.Future
 import groovy.transform.Canonical
 import groovy.transform.CompileStatic
 
-import jline.UnixTerminal
-import jline.console.UserInterruptException
-import jline.console.completer.ArgumentCompleter
-import jline.console.completer.Completer
-import jline.internal.NonBlockingInputStream
+import org.jline.reader.Completer
+import org.jline.reader.EndOfFileException
+import org.jline.reader.LineReader
+import org.jline.reader.LineReaderBuilder
+import org.jline.reader.UserInterruptException
+import org.jline.reader.impl.completer.AggregateCompleter
+import org.jline.reader.impl.completer.ArgumentCompleter
+import org.jline.terminal.Terminal
 import org.gradle.tooling.BuildActionExecuter
 import org.gradle.tooling.BuildCancelledException
 import org.gradle.tooling.ProjectConnection
@@ -246,8 +249,8 @@ class GrailsCli {
                 // force resolve of all profiles
                 profileRepository.getAllProfiles()
                 def commandNames = 
CommandRegistry.instance.findCommands(profileRepository).collect() { Command 
cmd -> cmd.name }
-                console.reader.addCompleter(new StringsCompleter(commandNames))
-                console.reader.addCompleter(new 
CommandCompleter(CommandRegistry.instance.findCommands(profileRepository)))
+                console.addCompleter(new StringsCompleter(commandNames))
+                console.addCompleter(new 
CommandCompleter(CommandRegistry.instance.findCommands(profileRepository)))
                 profile = [handleCommand: { ExecutionContext context ->
 
                     def cl = context.commandLine
@@ -394,8 +397,6 @@ class GrailsCli {
         System.setProperty(Environment.INTERACTIVE_MODE_ENABLED, 'true')
         GrailsConsole console = projectContext.console
 
-        def consoleReader = console.reader
-        consoleReader.setHandleUserInterrupt(true)
         def completers = aggregateCompleter.getCompleters()
 
         console.resetCompleters()
@@ -405,7 +406,7 @@ class GrailsCli {
         )
 
         completers.addAll((profile.getCompleters(projectContext) ?: []) as 
Collection<Completer>)
-        consoleReader.addCompleter(aggregateCompleter)
+        console.addCompleter(aggregateCompleter)
         return console
     }
 
@@ -420,7 +421,6 @@ class GrailsCli {
     }
 
     private void interactiveModeLoop(GrailsConsole console, ExecutorService 
commandExecutor) {
-        NonBlockingInputStream nonBlockingInput = (NonBlockingInputStream) 
console.reader.getInput()
         interactiveModeActive = true
         boolean firstRun = true
         while (keepRunning) {
@@ -434,48 +434,34 @@ class GrailsCli {
                     // CTRL-D was pressed, exit interactive mode
                     exitInteractiveMode()
                 } else if (commandLine.trim()) {
-                    if (nonBlockingInput.isNonBlockingEnabled()) {
-                        handleCommandWithCancellationSupport(console, 
commandLine, commandExecutor, nonBlockingInput)
-                    } else {
-                        handleCommand(cliParser.parseString(commandLine))
-                    }
+                    handleCommandWithCancellationSupport(console, commandLine, 
commandExecutor)
                 }
             } catch (BuildCancelledException cancelledException) {
                 console.updateStatus('Build stopped.')
             } catch (UserInterruptException e) {
                 exitInteractiveMode()
+            } catch (EndOfFileException e) {
+                exitInteractiveMode()
             } catch (Throwable e) {
                 console.error("Caught exception ${e.message}", e)
             }
         }
     }
 
-    private Boolean handleCommandWithCancellationSupport(GrailsConsole 
console, String commandLine, ExecutorService commandExecutor, 
NonBlockingInputStream nonBlockingInput) {
+    private Boolean handleCommandWithCancellationSupport(GrailsConsole 
console, String commandLine, ExecutorService commandExecutor) {
         ExecutionContext executionContext = 
createExecutionContext(cliParser.parseString(commandLine))
         Future<?> commandFuture = commandExecutor.submit({ 
handleCommand(executionContext) } as Callable<Boolean>)
-        def terminal = console.reader.terminal
-        if (terminal instanceof UnixTerminal) {
-            ((UnixTerminal) terminal).disableInterruptCharacter()
-        }
+        def terminal = console.terminal
         try {
             while (!commandFuture.done) {
-                if (nonBlockingInput.nonBlockingEnabled) {
-                    int peeked = nonBlockingInput.peek(100L)
-                    if (peeked > 0) {
-                        // read peeked character from buffer
-                        nonBlockingInput.read(1L)
-                        if (peeked == KEYPRESS_CTRL_C || peeked == 
KEYPRESS_ESC) {
-                            executionContext.console.log('  ')
-                            executionContext.console.updateStatus('Stopping 
build. Please wait...')
-                            executionContext.cancel()
-                        }
-                    }
-                }
-            }
-        } finally {
-            if (terminal instanceof UnixTerminal) {
-                ((UnixTerminal) terminal).enableInterruptCharacter()
+                // In JLine 3, we handle interrupts differently
+                // The terminal handles CTRL+C via signal handling
+                Thread.sleep(100)
             }
+        } catch (InterruptedException e) {
+            executionContext.console.log('  ')
+            executionContext.console.updateStatus('Stopping build. Please 
wait...')
+            executionContext.cancel()
         }
         if (!commandFuture.isCancelled()) {
             try {
@@ -625,17 +611,29 @@ class GrailsCli {
 
     protected Boolean bang(ExecutionContext context) {
         def console = context.console
-        def history = console.reader.history
+        def history = console.history
 
-        //move one step back to !
-        history.previous()
+        if (history == null || history.size() == 0) {
+            console.error('! not valid. Can not repeat without history')
+            return false
+        }
+
+        // Get previous command from history
+        def historyIterator = history.reverseIterator()
+        if (!historyIterator.hasNext()) {
+            console.error('! not valid. Can not repeat without history')
+            return false
+        }
 
-        if (!history.previous()) {
+        // Skip the current '!' command
+        historyIterator.next()
+        
+        if (!historyIterator.hasNext()) {
             console.error('! not valid. Can not repeat without history')
+            return false
         }
 
-        //another step to previous command
-        String historicalCommand = history.current()
+        String historicalCommand = historyIterator.next().line()
         if (historicalCommand.startsWith('!')) {
             console.error("Can not repeat command: $historicalCommand")
         } else {
diff --git 
a/grails-shell-cli/src/main/groovy/org/grails/cli/gradle/commands/GradleCommand.groovy
 
b/grails-shell-cli/src/main/groovy/org/grails/cli/gradle/commands/GradleCommand.groovy
index daa4708ff0..397a7e5c62 100644
--- 
a/grails-shell-cli/src/main/groovy/org/grails/cli/gradle/commands/GradleCommand.groovy
+++ 
b/grails-shell-cli/src/main/groovy/org/grails/cli/gradle/commands/GradleCommand.groovy
@@ -20,7 +20,10 @@ package org.grails.cli.gradle.commands
 
 import groovy.transform.CompileStatic
 
-import jline.console.completer.Completer
+import org.jline.reader.Candidate
+import org.jline.reader.Completer
+import org.jline.reader.LineReader
+import org.jline.reader.ParsedLine
 import org.gradle.tooling.BuildLauncher
 
 import org.grails.cli.gradle.GradleUtil
@@ -65,13 +68,12 @@ class GradleCommand implements ProjectCommand, Completer, 
ProjectContextAware {
     }
 
     @Override
-    int complete(String buffer, int cursor, List<CharSequence> candidates) {
+    void complete(LineReader reader, ParsedLine line, List<Candidate> 
candidates) {
         initializeCompleter()
 
-        if (completer)
-            return completer.complete(buffer, cursor, candidates)
-        else
-            return cursor
+        if (completer) {
+            completer.complete(reader, line, candidates)
+        }
     }
 
     private void initializeCompleter() {
diff --git 
a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/ClosureCompleter.groovy
 
b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/ClosureCompleter.groovy
index 7ccbb5280e..a73248e82e 100644
--- 
a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/ClosureCompleter.groovy
+++ 
b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/ClosureCompleter.groovy
@@ -20,7 +20,10 @@ package org.grails.cli.interactive.completers
 
 import groovy.transform.CompileStatic
 
-import jline.console.completer.Completer
+import org.jline.reader.Candidate
+import org.jline.reader.Completer
+import org.jline.reader.LineReader
+import org.jline.reader.ParsedLine
 
 /**
  * @author Graeme Rocher
@@ -38,13 +41,13 @@ class ClosureCompleter implements Completer {
 
     Completer getCompleter() {
         if (completer == null) {
-            completer = new 
jline.console.completer.StringsCompleter(closure.call())
+            completer = new StringsCompleter(closure.call())
         }
         completer
     }
 
     @Override
-    int complete(String buffer, int cursor, List<CharSequence> candidates) {
-        getCompleter().complete(buffer, cursor, candidates)
+    void complete(LineReader reader, ParsedLine line, List<Candidate> 
candidates) {
+        getCompleter().complete(reader, line, candidates)
     }
 }
diff --git 
a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/EscapingFileNameCompletor.groovy
 
b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/EscapingFileNameCompletor.groovy
index 68f811bf25..3d8c7e7215 100644
--- 
a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/EscapingFileNameCompletor.groovy
+++ 
b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/EscapingFileNameCompletor.groovy
@@ -18,10 +18,13 @@
  */
 package org.grails.cli.interactive.completers
 
-import jline.console.completer.FileNameCompleter
+import org.jline.builtins.Completers.FileNameCompleter
+import org.jline.reader.Candidate
+import org.jline.reader.LineReader
+import org.jline.reader.ParsedLine
 
 /**
- * JLine Completor that does file path matching like FileNameCompletor,
+ * JLine Completor that does file path matching like FileNameCompleter,
  * but in addition it escapes whitespace in completions with the '\'
  * character.
  *
@@ -31,19 +34,22 @@ import jline.console.completer.FileNameCompleter
 class EscapingFileNameCompletor extends FileNameCompleter {
 
     /**
-     * <p>Gets FileNameCompletor to create a list of candidates and then
+     * <p>Gets FileNameCompleter to create a list of candidates and then
      * inserts '\' before any whitespace characters in each of the candidates.
      * If a candidate ends in a whitespace character, then that is <em>not</em>
      * escaped.</p>
      */
-    int complete(String buffer, int cursor, List candidates) {
-        int retval = super.complete(buffer, cursor, candidates)
+    @Override
+    void complete(LineReader reader, ParsedLine line, List<Candidate> 
candidates) {
+        List<Candidate> tempCandidates = []
+        super.complete(reader, line, tempCandidates)
 
-        int count = candidates.size()
-        for (int i = 0; i < count; i++) {
-            candidates[i] = candidates[i].replaceAll(/(\s)(?!$)/, '\\\\$1')
+        for (Candidate candidate : tempCandidates) {
+            String value = candidate.value()
+            // Escape whitespace in the value, except for trailing whitespace
+            String escapedValue = value.replaceAll(/(\s)(?!$)/, '\\\\$1')
+            candidates.add(new Candidate(escapedValue, candidate.displ(), 
candidate.group(), 
+                    candidate.descr(), candidate.suffix(), candidate.key(), 
candidate.complete()))
         }
-
-        return retval
     }
 }
diff --git 
a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/RegexCompletor.groovy
 
b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/RegexCompletor.groovy
index abba20c368..f8486b89f7 100644
--- 
a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/RegexCompletor.groovy
+++ 
b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/RegexCompletor.groovy
@@ -20,7 +20,10 @@ package org.grails.cli.interactive.completers
 
 import java.util.regex.Pattern
 
-import jline.console.completer.Completer
+import org.jline.reader.Candidate
+import org.jline.reader.Completer
+import org.jline.reader.LineReader
+import org.jline.reader.ParsedLine
 
 /**
  * JLine Completor that accepts a string if it matches a given regular
@@ -44,17 +47,16 @@ class RegexCompletor implements Completer {
     /**
      * <p>Check whether the whole buffer matches the configured pattern.
      * If it does, the buffer is added to the <tt>candidates</tt> list
-     * (which indicates acceptance of the buffer string) and returns 0,
-     * i.e. the start of the buffer. This mimics the behaviour of 
SimpleCompletor.
+     * (which indicates acceptance of the buffer string).
      * </p>
-     * <p>If the buffer doesn't match the configured pattern, this returns
-     * -1 and the <tt>candidates</tt> list is left empty.</p>
+     * <p>If the buffer doesn't match the configured pattern, the
+     * <tt>candidates</tt> list is left empty.</p>
      */
-    int complete(String buffer, int cursor, List candidates) {
+    @Override
+    void complete(LineReader reader, ParsedLine line, List<Candidate> 
candidates) {
+        String buffer = line.word()
         if (buffer ==~ pattern) {
-            candidates << buffer
-            return 0
+            candidates.add(new Candidate(buffer))
         }
-        else return -1
     }
 }
diff --git 
a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/SimpleOrFileNameCompletor.groovy
 
b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/SimpleOrFileNameCompletor.groovy
index 34829f47c0..cffa8f29ef 100644
--- 
a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/SimpleOrFileNameCompletor.groovy
+++ 
b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/SimpleOrFileNameCompletor.groovy
@@ -18,8 +18,10 @@
  */
 package org.grails.cli.interactive.completers
 
-import jline.console.completer.Completer
-import jline.console.completer.StringsCompleter
+import org.jline.reader.Candidate
+import org.jline.reader.Completer
+import org.jline.reader.LineReader
+import org.jline.reader.ParsedLine
 
 /**
  * JLine Completor that mixes a fixed set of options with file path matches.
@@ -30,8 +32,8 @@ import jline.console.completer.StringsCompleter
  */
 class SimpleOrFileNameCompletor implements Completer {
 
-    private simpleCompletor
-    private fileNameCompletor
+    private Completer simpleCompletor
+    private Completer fileNameCompletor
 
     SimpleOrFileNameCompletor(List fixedOptions) {
         this(fixedOptions as String[])
@@ -42,20 +44,17 @@ class SimpleOrFileNameCompletor implements Completer {
         fileNameCompletor = new EscapingFileNameCompletor()
     }
 
-    int complete(String buffer, int cursor, List candidates) {
+    @Override
+    void complete(LineReader reader, ParsedLine line, List<Candidate> 
candidates) {
         // Try the simple completor first...
-        def retval = simpleCompletor.complete(buffer, cursor, candidates)
+        List<Candidate> simpleCandidates = []
+        simpleCompletor.complete(reader, line, simpleCandidates)
+        candidates.addAll(simpleCandidates)
 
         // ...and then the file path completor. By using the given candidate
         // list with both completors we aggregate the results automatically.
-        def fileRetval = fileNameCompletor.complete(buffer, cursor, candidates)
-
-        // If the simple completor has matched, we return its value, otherwise
-        // we return whatever the file path matcher returned. This ensures that
-        // both simple completor and file path completor candidates appear
-        // correctly in the command prompt. If neither competors have matches,
-        // we of course return -1.
-        if (retval == -1) retval = fileRetval
-        return candidates ? retval : -1
+        List<Candidate> fileCandidates = []
+        fileNameCompletor.complete(reader, line, fileCandidates)
+        candidates.addAll(fileCandidates)
     }
 }
diff --git 
a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/SortedAggregateCompleter.java
 
b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/SortedAggregateCompleter.java
index becaf7f130..73a9b31265 100644
--- 
a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/SortedAggregateCompleter.java
+++ 
b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/SortedAggregateCompleter.java
@@ -21,24 +21,23 @@ package org.grails.cli.interactive.completers;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.LinkedList;
+import java.util.Comparator;
 import java.util.List;
-import java.util.SortedSet;
-import java.util.TreeSet;
+import java.util.Objects;
 
-import jline.console.completer.Completer;
-
-import static jline.internal.Preconditions.checkNotNull;
+import org.jline.reader.Candidate;
+import org.jline.reader.Completer;
+import org.jline.reader.LineReader;
+import org.jline.reader.ParsedLine;
 
 /**
- * Copied from jline AggregateCompleter
- *
- * sorts aggregated completions
+ * An aggregate completer that sorts completion candidates.
  *
+ * @author Graeme Rocher
+ * @since 3.0
  */
-public class SortedAggregateCompleter
-    implements Completer
-{
+public class SortedAggregateCompleter implements Completer {
+    
     private final List<Completer> completers = new ArrayList<>();
 
     public SortedAggregateCompleter() {
@@ -52,7 +51,7 @@ public class SortedAggregateCompleter
      * @param completers the collection of completers
      */
     public SortedAggregateCompleter(final Collection<Completer> completers) {
-        checkNotNull(completers);
+        Objects.requireNonNull(completers);
         this.completers.addAll(completers);
     }
 
@@ -77,40 +76,24 @@ public class SortedAggregateCompleter
 
     /**
      * Perform a completion operation across all aggregated completers.
-     *
-     * @see Completer#complete(String, int, java.util.List)
-     * @return the highest completion return value from all completers
      */
-    public int complete(final String buffer, final int cursor, final 
List<CharSequence> candidates) {
-        // buffer could be null
-        checkNotNull(candidates);
+    @Override
+    public void complete(LineReader reader, ParsedLine line, List<Candidate> 
candidates) {
+        Objects.requireNonNull(candidates);
 
-        List<Completion> completions = new ArrayList<>(completers.size());
+        List<Candidate> allCandidates = new ArrayList<>();
 
-        // Run each completer, saving its completion results
-        int max = -1;
+        // Run each completer, collecting candidates
         for (Completer completer : completers) {
-            Completion completion = new Completion(candidates);
-            completion.complete(completer, buffer, cursor);
-
-            // Compute the max cursor position
-            max = Math.max(max, completion.cursor);
-
-            completions.add(completion);
+            List<Candidate> completerCandidates = new ArrayList<>();
+            completer.complete(reader, line, completerCandidates);
+            allCandidates.addAll(completerCandidates);
         }
 
-        SortedSet<CharSequence> allCandidates = new TreeSet<>();
-
-        // Append candidates from completions which have the same cursor 
position as max
-        for (Completion completion : completions) {
-            if (completion.cursor == max) {
-                allCandidates.addAll(completion.candidates);
-            }
-        }
+        // Sort the candidates by their value
+        allCandidates.sort(Comparator.comparing(Candidate::value));
 
         candidates.addAll(allCandidates);
-
-        return max;
     }
 
     /**
@@ -122,21 +105,4 @@ public class SortedAggregateCompleter
             "completers=" + completers +
             '}';
     }
-
-    private class Completion
-    {
-        public final List<CharSequence> candidates;
-
-        public int cursor;
-
-        public Completion(final List<CharSequence> candidates) {
-            checkNotNull(candidates);
-            this.candidates = new LinkedList<>(candidates);
-        }
-
-        public void complete(final Completer completer, final String buffer, 
final int cursor) {
-            checkNotNull(completer);
-            this.cursor = completer.complete(buffer, cursor, candidates);
-        }
-    }
 }
diff --git 
a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/StringsCompleter.java
 
b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/StringsCompleter.java
index 06adbc6015..db83a20b07 100644
--- 
a/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/StringsCompleter.java
+++ 
b/grails-shell-cli/src/main/groovy/org/grails/cli/interactive/completers/StringsCompleter.java
@@ -21,12 +21,14 @@ package org.grails.cli.interactive.completers;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
 import java.util.SortedSet;
 import java.util.TreeSet;
 
-import jline.console.completer.Completer;
-
-import static jline.internal.Preconditions.checkNotNull;
+import org.jline.reader.Candidate;
+import org.jline.reader.Completer;
+import org.jline.reader.LineReader;
+import org.jline.reader.ParsedLine;
 
 /**
  * A completer that completes based on a collection of Strings
@@ -34,9 +36,8 @@ import static jline.internal.Preconditions.checkNotNull;
  * @author Graeme Rocher
  * @since 3.0
  */
-public class StringsCompleter
-    implements Completer
-{
+public class StringsCompleter implements Completer {
+    
     private SortedSet<String> strings = new TreeSet<>();
 
     public StringsCompleter() {
@@ -44,7 +45,7 @@ public class StringsCompleter
     }
 
     public StringsCompleter(final Collection<String> strings) {
-        checkNotNull(strings);
+        Objects.requireNonNull(strings);
         getStrings().addAll(strings);
     }
 
@@ -60,23 +61,23 @@ public class StringsCompleter
         this.strings = strings;
     }
 
-    public int complete(final String buffer, final int cursor, final 
List<CharSequence> candidates) {
-        // buffer could be null
-        checkNotNull(candidates);
+    @Override
+    public void complete(LineReader reader, ParsedLine line, List<Candidate> 
candidates) {
+        Objects.requireNonNull(candidates);
 
-        if (buffer == null) {
-            candidates.addAll(getStrings());
-        }
-        else {
+        String buffer = line.word();
+        
+        if (buffer == null || buffer.isEmpty()) {
+            for (String string : getStrings()) {
+                candidates.add(new Candidate(string));
+            }
+        } else {
             for (String match : getStrings().tailSet(buffer)) {
                 if (!match.startsWith(buffer)) {
                     break;
                 }
-
-                candidates.add(match);
+                candidates.add(new Candidate(match));
             }
         }
-
-        return candidates.isEmpty() ? -1 : 0;
     }
 }
diff --git 
a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/AbstractProfile.groovy
 
b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/AbstractProfile.groovy
index 6b67ce2e62..dd76a780a2 100644
--- 
a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/AbstractProfile.groovy
+++ 
b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/AbstractProfile.groovy
@@ -19,8 +19,8 @@ package org.grails.cli.profile
 import groovy.transform.CompileStatic
 import groovy.transform.ToString
 
-import jline.console.completer.ArgumentCompleter
-import jline.console.completer.Completer
+import org.jline.reader.Completer
+import org.jline.reader.impl.completer.ArgumentCompleter
 import org.eclipse.aether.artifact.DefaultArtifact
 import org.eclipse.aether.graph.Dependency
 import org.eclipse.aether.graph.Exclusion
diff --git 
a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/Profile.java 
b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/Profile.java
index fe395824b1..275089b904 100644
--- a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/Profile.java
+++ b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/Profile.java
@@ -22,7 +22,7 @@ import java.io.File;
 import java.util.List;
 import java.util.Set;
 
-import jline.console.completer.Completer;
+import org.jline.reader.Completer;
 import org.eclipse.aether.graph.Dependency;
 
 import org.grails.config.NavigableMap;
@@ -129,7 +129,7 @@ public interface Profile {
     /**
      * The profile completers
      * @param context The {@link org.grails.cli.profile.ProjectContext} 
instance
-     * @return An {@link java.lang.Iterable} of {@link 
jline.console.completer.Completer} instances
+     * @return An {@link java.lang.Iterable} of {@link 
org.jline.reader.Completer} instances
      */
     Iterable<Completer> getCompleters(ProjectContext context);
 
diff --git 
a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/ArgumentCompletingCommand.groovy
 
b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/ArgumentCompletingCommand.groovy
index 692aed823c..b85435f55d 100644
--- 
a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/ArgumentCompletingCommand.groovy
+++ 
b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/ArgumentCompletingCommand.groovy
@@ -19,7 +19,10 @@
 
 package org.grails.cli.profile.commands
 
-import jline.console.completer.Completer
+import org.jline.reader.Candidate
+import org.jline.reader.Completer
+import org.jline.reader.LineReader
+import org.jline.reader.ParsedLine
 
 import org.grails.build.parsing.CommandLine
 import org.grails.build.parsing.CommandLineParser
@@ -34,13 +37,13 @@ abstract class ArgumentCompletingCommand implements 
Command, Completer {
     CommandLineParser cliParser = new CommandLineParser()
 
     @Override
-    final int complete(String buffer, int cursor, List<CharSequence> 
candidates) {
+    final void complete(LineReader reader, ParsedLine line, List<Candidate> 
candidates) {
         def desc = getDescription()
-        def commandLine = cliParser.parseString(buffer)
-        return complete(commandLine, desc, candidates, cursor)
+        def commandLine = cliParser.parseString(line.line())
+        complete(commandLine, desc, candidates)
     }
 
-    protected int complete(CommandLine commandLine, CommandDescription desc, 
List<CharSequence> candidates, int cursor) {
+    protected void complete(CommandLine commandLine, CommandDescription desc, 
List<Candidate> candidates) {
         def invalidOptions = commandLine.undeclaredOptions.keySet().findAll { 
String str ->
             desc.getFlag(str.trim()) == null
         }
@@ -54,15 +57,14 @@ abstract class ArgumentCompletingCommand implements 
Command, Completer {
                 if (lastOption) {
                     def lastArg = lastOption.key
                     if (arg.name.startsWith(lastArg)) {
-                        candidates.add("${argName.substring(lastArg.length())} 
".toString())
+                        candidates.add(new 
Candidate("${argName.substring(lastArg.length())} ".toString()))
                     } else if (!invalidOptions) {
-                        candidates.add("$flag ".toString())
+                        candidates.add(new Candidate("$flag ".toString()))
                     }
                 } else {
-                    candidates.add("$flag ".toString())
+                    candidates.add(new Candidate("$flag ".toString()))
                 }
             }
         }
-        return cursor
     }
 }
diff --git 
a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/CommandCompleter.groovy
 
b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/CommandCompleter.groovy
index 1520a6caf6..b590e2fe07 100644
--- 
a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/CommandCompleter.groovy
+++ 
b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/CommandCompleter.groovy
@@ -18,7 +18,10 @@
  */
 package org.grails.cli.profile.commands
 
-import jline.console.completer.Completer
+import org.jline.reader.Candidate
+import org.jline.reader.Completer
+import org.jline.reader.LineReader
+import org.jline.reader.ParsedLine
 
 import org.grails.cli.profile.Command
 
@@ -37,7 +40,8 @@ class CommandCompleter implements Completer {
     }
 
     @Override
-    int complete(String buffer, int cursor, List<CharSequence> candidates) {
+    void complete(LineReader reader, ParsedLine line, List<Candidate> 
candidates) {
+        String buffer = line.line()
         def cmd = commands.find() {
             def trimmed = buffer.trim()
             if (trimmed.split(/\s/).size() > 1) {
@@ -48,8 +52,7 @@ class CommandCompleter implements Completer {
             }
         }
         if (cmd instanceof Completer) {
-            return ((Completer) cmd).complete(buffer, cursor, candidates)
+            ((Completer) cmd).complete(reader, line, candidates)
         }
-        return cursor
     }
 }
diff --git 
a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/CreateAppCommand.groovy
 
b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/CreateAppCommand.groovy
index 1766cf4ab7..8747f5d3c2 100644
--- 
a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/CreateAppCommand.groovy
+++ 
b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/CreateAppCommand.groovy
@@ -101,7 +101,7 @@ class CreateAppCommand extends ArgumentCompletingCommand 
implements ProfileRepos
     }
 
     @Override
-    protected int complete(CommandLine commandLine, CommandDescription desc, 
List<CharSequence> candidates, int cursor) {
+    protected void complete(CommandLine commandLine, CommandDescription desc, 
List<org.jline.reader.Candidate> candidates) {
         def lastOption = commandLine.lastOption()
         if (lastOption != null) {
             // if value == true it means no profile is specified and only the 
flag is present
@@ -109,8 +109,8 @@ class CreateAppCommand extends ArgumentCompletingCommand 
implements ProfileRepos
             if (lastOption.key == PROFILE_FLAG) {
                 def val = lastOption.value
                 if (val == true) {
-                    candidates.addAll(profileNames)
-                    return cursor
+                    profileNames.each { candidates.add(new 
org.jline.reader.Candidate(it)) }
+                    return
                 } else if (!profileNames.contains(val)) {
                     def valStr = val.toString()
 
@@ -119,24 +119,24 @@ class CreateAppCommand extends ArgumentCompletingCommand 
implements ProfileRepos
                     }.collect() { String pn ->
                         "${pn.substring(valStr.size())} ".toString()
                     }
-                    candidates.addAll(candidateProfiles)
-                    return cursor
+                    candidateProfiles.each { candidates.add(new 
org.jline.reader.Candidate(it)) }
+                    return
                 }
             } else if (lastOption.key == FEATURES_FLAG) {
                 def val = lastOption.value
                 def profile = 
profileRepository.getProfile(commandLine.hasOption(PROFILE_FLAG) ? 
commandLine.optionValue(PROFILE_FLAG).toString() : getDefaultProfile())
                 def featureNames = profile.features.collect() { Feature f -> 
f.name }
                 if (val == true) {
-                    candidates.addAll(featureNames)
-                    return cursor
+                    featureNames.each { candidates.add(new 
org.jline.reader.Candidate(it)) }
+                    return
                 } else if (!profileNames.contains(val)) {
                     def valStr = val.toString()
                     if (valStr.endsWith(',')) {
                         def specified = valStr.split(',')
-                        candidates.addAll(featureNames.findAll { String f ->
+                        featureNames.findAll { String f ->
                             !specified.contains(f)
-                        })
-                        return cursor
+                        }.each { candidates.add(new 
org.jline.reader.Candidate(it)) }
+                        return
                     }
 
                     def candidatesFeatures = featureNames.findAll { String pn 
->
@@ -144,12 +144,12 @@ class CreateAppCommand extends ArgumentCompletingCommand 
implements ProfileRepos
                     }.collect() { String pn ->
                         "${pn.substring(valStr.size())} ".toString()
                     }
-                    candidates.addAll(candidatesFeatures)
-                    return cursor
+                    candidatesFeatures.each { candidates.add(new 
org.jline.reader.Candidate(it)) }
+                    return
                 }
             }
         }
-        return super.complete(commandLine, desc, candidates, cursor)
+        super.complete(commandLine, desc, candidates)
     }
 
     protected File getDestinationDirectory(File srcFile) {
diff --git 
a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/DefaultMultiStepCommand.groovy
 
b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/DefaultMultiStepCommand.groovy
index c5f29586de..30f5d06e49 100644
--- 
a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/DefaultMultiStepCommand.groovy
+++ 
b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/DefaultMultiStepCommand.groovy
@@ -20,7 +20,7 @@ package org.grails.cli.profile.commands
 
 import groovy.transform.CompileDynamic
 
-import jline.console.completer.Completer
+import org.jline.reader.Completer
 
 import grails.build.logging.GrailsConsole
 import org.grails.cli.profile.AbstractStep
diff --git 
a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/HelpCommand.groovy
 
b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/HelpCommand.groovy
index 698a3cb420..f107c7163e 100644
--- 
a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/HelpCommand.groovy
+++ 
b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/HelpCommand.groovy
@@ -16,7 +16,10 @@
  */
 package org.grails.cli.profile.commands
 
-import jline.console.completer.Completer
+import org.jline.reader.Candidate
+import org.jline.reader.Completer
+import org.jline.reader.LineReader
+import org.jline.reader.ParsedLine
 
 import org.grails.build.parsing.CommandLine
 import org.grails.build.parsing.CommandLineParser
@@ -113,20 +116,20 @@ grails [environment]* [target] [arguments]*'
     }
 
     @Override
-    int complete(String buffer, int cursor, List<CharSequence> candidates) {
+    void complete(LineReader reader, ParsedLine line, List<Candidate> 
candidates) {
         def allCommands = findAllCommands().collect() { CommandDescription 
desc -> desc.name }
+        String buffer = line.word()
 
         for (cmd in allCommands) {
             if (buffer) {
                 if (cmd.startsWith(buffer)) {
-                    candidates << cmd.substring(buffer.size())
+                    candidates.add(new Candidate(cmd))
                 }
             }
             else {
-                candidates << cmd
+                candidates.add(new Candidate(cmd))
             }
         }
-        return cursor
     }
 
     protected Collection<CommandDescription> findAllCommands() {
diff --git 
a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/OpenCommand.groovy
 
b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/OpenCommand.groovy
index 6ebb3f971d..5bff06d6e8 100644
--- 
a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/OpenCommand.groovy
+++ 
b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/OpenCommand.groovy
@@ -23,8 +23,11 @@ import java.awt.Desktop
 
 import groovy.transform.CompileStatic
 
-import jline.console.completer.Completer
-import jline.console.completer.FileNameCompleter
+import org.jline.builtins.Completers.FileNameCompleter
+import org.jline.reader.Candidate
+import org.jline.reader.Completer
+import org.jline.reader.LineReader
+import org.jline.reader.ParsedLine
 
 import org.grails.cli.profile.CommandDescription
 import org.grails.cli.profile.ExecutionContext
@@ -71,7 +74,7 @@ class OpenCommand implements ProjectCommand, Completer {
     }
 
     @Override
-    int complete(String buffer, int cursor, List<CharSequence> candidates) {
-        return new FileNameCompleter().complete(buffer, cursor, candidates)
+    void complete(LineReader reader, ParsedLine line, List<Candidate> 
candidates) {
+        new FileNameCompleter().complete(reader, line, candidates)
     }
 }
diff --git a/grails-shell-cli/src/test/groovy/org/grails/cli/TestTerminal.java 
b/grails-shell-cli/src/test/groovy/org/grails/cli/TestTerminal.java
index f72eb8ae2a..a4b9fd8d77 100644
--- a/grails-shell-cli/src/test/groovy/org/grails/cli/TestTerminal.java
+++ b/grails-shell-cli/src/test/groovy/org/grails/cli/TestTerminal.java
@@ -18,12 +18,29 @@
  */
 package org.grails.cli;
 
-import jline.TerminalSupport;
+import org.jline.terminal.Terminal;
+import org.jline.terminal.TerminalBuilder;
 
-public class TestTerminal extends TerminalSupport {
-    public TestTerminal() {
-        super(true);
-        setAnsiSupported(false);
-        setEchoEnabled(false);
+import java.io.IOException;
+
+/**
+ * A terminal for testing purposes that creates a dumb terminal.
+ */
+public class TestTerminal {
+    
+    private final Terminal terminal;
+    
+    public TestTerminal() throws IOException {
+        this.terminal = TerminalBuilder.builder()
+                .dumb(true)
+                .build();
+    }
+    
+    public Terminal getTerminal() {
+        return terminal;
+    }
+    
+    public void close() throws IOException {
+        terminal.close();
     }
 }
diff --git 
a/grails-shell-cli/src/test/groovy/org/grails/cli/interactive/completers/RegexCompletorSpec.groovy
 
b/grails-shell-cli/src/test/groovy/org/grails/cli/interactive/completers/RegexCompletorSpec.groovy
index 01800e8234..34c9f12571 100644
--- 
a/grails-shell-cli/src/test/groovy/org/grails/cli/interactive/completers/RegexCompletorSpec.groovy
+++ 
b/grails-shell-cli/src/test/groovy/org/grails/cli/interactive/completers/RegexCompletorSpec.groovy
@@ -18,39 +18,47 @@
  */
 package org.grails.cli.interactive.completers
 
+import org.jline.reader.Candidate
+import org.jline.reader.LineReader
+import org.jline.reader.ParsedLine
 import spock.lang.*
 
 class RegexCompletorSpec extends Specification {
-    @Unroll("String '#source' is not matching")
+
+    @Unroll("String '#source' is matching")
     def "Simple pattern matches"() {
         given: "a regex completor and an empty candidate list"
         def completor = new RegexCompletor(/!\w+/)
         def candidateList = []
+        def parsedLine = Stub(ParsedLine) {
+            word() >> source
+        }
 
         when: "the completor is invoked for a given string"
-        def retval = completor.complete(source, 0, candidateList)
+        completor.complete(null, parsedLine, candidateList)
 
-        then: "that string is the sole candidate and the return value is 0"
+        then: "that string is the sole candidate"
         candidateList.size() == 1
-        candidateList[0] == source
-        retval == 0
+        candidateList[0].value() == source
 
         where:
         source << [ "!ls", "!test_stuff" ]
     }
 
-    @Unroll("String '#source' is incorrectly matching")
+    @Unroll("String '#source' is not matching")
     def "Non matching strings"() {
         given: "a regex completor and an empty candidate list"
         def completor = new RegexCompletor(/!\w+/)
         def candidateList = []
+        def parsedLine = Stub(ParsedLine) {
+            word() >> source
+        }
 
         when: "the completor is invoked for a given (non-matching) string"
-        def retval = completor.complete(source, 0, candidateList)
+        completor.complete(null, parsedLine, candidateList)
 
-        then: "the candidate list is empty and the return value is -1"
+        then: "the candidate list is empty"
         candidateList.size() == 0
-        retval == -1
 
         where:
         source << [ "!ls ls", "!", "test", "" ]
diff --git a/grails-test-core/build.gradle b/grails-test-core/build.gradle
index 6d21f9a0e3..ac147627d2 100644
--- a/grails-test-core/build.gradle
+++ b/grails-test-core/build.gradle
@@ -51,7 +51,7 @@ dependencies {
     api 'org.apache.groovy:groovy'
 
     // command line requirements
-    api  'jline:jline'
+    api  'org.jline:jline'
     api 'org.fusesource.jansi:jansi'
 
     // Ant
diff --git a/grails-web-url-mappings/build.gradle 
b/grails-web-url-mappings/build.gradle
index 44461d1d85..3b2aa68bf7 100644
--- a/grails-web-url-mappings/build.gradle
+++ b/grails-web-url-mappings/build.gradle
@@ -46,7 +46,7 @@ dependencies {
 
     compileOnly 'org.fusesource.jansi:jansi'
     testRuntimeOnly 'org.fusesource.jansi:jansi'
-    compileOnly 'jline:jline'
+    compileOnly 'org.jline:jline'
 
     implementation 'com.github.ben-manes.caffeine:caffeine'
 

Reply via email to