/*
 * sbt
 * Copyright 2023, Scala center
 * Copyright 2011 - 2022, Lightbend, Inc.
 * Copyright 2008 - 2010, Mark Harrah
 * Licensed under Apache License 2.0 (see LICENSE)
 */

package sbt

import java.io.{ File, IOException }
import java.net.URI
import java.nio.channels.ClosedChannelException
import java.nio.file.{ FileAlreadyExistsException, FileSystems, Files, Paths }
import java.util.Properties
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.atomic.AtomicBoolean

import sbt.Project.LoadAction
import sbt.ProjectExtra.*
import sbt.ScopeAxis.Select
import sbt.internal.Aggregation.AnyKeys
import sbt.internal.*
import sbt.internal.client.BspClient
import sbt.internal.inc.ScalaInstance
import sbt.internal.io.Retry
import sbt.internal.nio.{ CheckBuildSources, FileTreeRepository }
import sbt.internal.server.{ BuildServerProtocol, NetworkChannel }
import sbt.internal.util.Terminal.hasConsole
import sbt.internal.util.Types.{ const, idFun }
import sbt.internal.util.complete.Parser
import sbt.internal.util.{ RunningProcesses, Terminal as ITerminal, * }
import sbt.io.*
import sbt.io.syntax.*
import sbt.util.{ ActionCache, HashUtil, Level, Logger, Show }
import xsbti.AppProvider

import scala.annotation.{ nowarn, tailrec }
import scala.concurrent.ExecutionContext
import scala.concurrent.duration.Duration
import scala.reflect.ClassTag
import scala.util.control.NonFatal
import scala.util.boundary

/** This class is the entry point for sbt. */
final class xMain extends xsbti.AppMain:
  def run(configuration: xsbti.AppConfiguration): xsbti.MainResult =
    new XMainConfiguration().run("xMain", configuration)
end xMain

private[sbt] object xMain:
  private[sbt] def dealiasBaseDirectory(config: xsbti.AppConfiguration): xsbti.AppConfiguration =
    val dealiasedBase = config.baseDirectory.getCanonicalFile
    if config.baseDirectory == dealiasedBase then config
    else
      new xsbti.AppConfiguration {
        override def arguments: Array[String] = config.arguments()
        override val baseDirectory: File = dealiasedBase
        override def provider: AppProvider = config.provider()
      }

  private[sbt] def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = boundary {
    try {
      if (!sys.props.contains("jna.nosys")) sys.props.put("jna.nosys", "true")
      import BasicCommandStrings.{ JavaClient, DashDashClient, DashDashServer, runEarly }
      import BasicCommands.early
      import BuiltinCommands.defaults
      import sbt.internal.CommandStrings.{ BootCommand, DefaultsCommand, InitCommand }
      import sbt.internal.client.NetworkClient

      Plugins.defaultRequires = sbt.plugins.JvmPlugin
      // if we detect -Dsbt.client=true or -client, run thin client.
      val clientModByEnv = SysProp.client
      val userCommands = configuration.arguments
        .map(_.trim)
        .filterNot(_ == DashDashServer)
      val isClient: String => Boolean = cmd => (cmd == JavaClient) || (cmd == DashDashClient)
      val isBsp: String => Boolean = cmd => (cmd == "-bsp") || (cmd == "--bsp") || (cmd == "bsp")
      val isNew: String => Boolean = cmd => (cmd == "new") || (cmd == "init")
      lazy val isServer = !userCommands.exists(c => isBsp(c) || isClient(c))
      // keep this lazy to prevent project directory created prematurely
      // Only create boot server socket if server mode is enabled and server autostart is enabled
      lazy val bootServerSocket =
        if (isServer && SysProp.serverAutoStart) getSocketOrExit(configuration) match {
          case (_, Some(e)) => boundary.break(e)
          case (s, _)       => s
        }
        else None
      lazy val detachStdio = userCommands.exists(_ == BasicCommandStrings.DashDashDetachStdio)
      def withStreams[A](f: => A): A =
        try {
          bootServerSocket.foreach(l => ITerminal.setBootStreams(l.inputStream, l.outputStream))
          ITerminal.withStreams(true, isSubProcess = detachStdio) {
            f
          }
        } finally {
          if (ITerminal.isAnsiSupported) {
            // Clear any stray progress lines
            System.out.print(ConsoleAppender.ClearScreenAfterCursor)
            System.out.flush()
          }
        }

      userCommands match {
        case cmds if cmds.exists(isBsp) =>
          BspClient.run(dealiasBaseDirectory(configuration))
        case cmds if cmds.exists(isNew) =>
          IO.withTemporaryDirectory { tempDir =>
            val rebasedConfig = new xsbti.AppConfiguration {
              override def arguments: Array[String] = configuration.arguments()
              override val baseDirectory: File = tempDir / "new"
              override def provider: AppProvider = configuration.provider()
            }
            val state = StandardMain
              .initialState(
                rebasedConfig,
                Seq(defaults, early),
                runEarly(DefaultsCommand) :: runEarly("error") :: runEarly(
                  InitCommand
                ) :: BootCommand :: Nil
              )
              .put(BasicKeys.detachStdio, detachStdio)
            StandardMain.runManaged(state)
          }
        case _ if clientModByEnv || userCommands.exists(isClient) =>
          withStreams {
            val args = userCommands.toList.filterNot(isClient)
            Exit(NetworkClient.run(dealiasBaseDirectory(configuration), args))
          }
        case _ =>
          withStreams {
            val state0 = StandardMain
              .initialState(
                dealiasBaseDirectory(configuration),
                Seq(defaults, early),
                runEarly(DefaultsCommand) :: runEarly(InitCommand) :: BootCommand :: Nil
              )
              .put(BasicKeys.detachStdio, detachStdio)
            val state = bootServerSocket match {
              case Some(l) => state0.put(Keys.bootServerSocket, l)
              case _       => state0
            }
            try StandardMain.runManaged(state)
            finally bootServerSocket.foreach(_.close())
          }
      }
    } finally {
      ShutdownHooks.close()
    }
  }

  private def getSocketOrExit(
      configuration: xsbti.AppConfiguration
  ): (Option[BootServerSocket], Option[Exit]) = {
    def printThrowable(e: Throwable): Unit = {
      println("sbt thinks that server is already booting because of this exception:")
      e.printStackTrace()
    }

    val target =
      configuration.baseDirectory().toPath().toRealPath().resolve("project").resolve("target")
    val hash = HashUtil.farmHash(target.toString().getBytes("UTF-8"));
    try Some(new BootServerSocket(configuration, hash)) -> None
    catch {
      case e: ServerAlreadyBootingException if hasConsole && !ITerminal.startedByRemoteClient =>
        printThrowable(e)
        println("Create a new server? y/n (default y)")
        val exit =
          if (ITerminal.get.withRawInput(System.in.read) == 'n'.toInt) Some(Exit(1))
          else None
        (None, exit)
      case e: ServerAlreadyBootingException =>
        if (SysProp.forceServerStart) (None, None)
        else {
          printThrowable(e)
          (None, Some(Exit(2)))
        }
      case _: UnsatisfiedLinkError => (None, None)
    }
  }
end xMain

final class ScriptMain extends xsbti.AppMain {
  def run(configuration: xsbti.AppConfiguration): xsbti.MainResult =
    new XMainConfiguration().run("ScriptMain", configuration)
}
private[sbt] object ScriptMain {
  private[sbt] def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = {
    import BasicCommandStrings.runEarly
    val state = StandardMain.initialState(
      xMain.dealiasBaseDirectory(configuration),
      BuiltinCommands.ScriptCommands,
      runEarly(Level.Error.toString) :: Script.Name :: Nil
    )
    StandardMain.runManaged(state)
  }
}

final class ConsoleMain extends xsbti.AppMain {
  def run(configuration: xsbti.AppConfiguration): xsbti.MainResult =
    new XMainConfiguration().run("ConsoleMain", configuration)
}
private[sbt] object ConsoleMain {
  private[sbt] def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = {
    val state = StandardMain.initialState(
      xMain.dealiasBaseDirectory(configuration),
      BuiltinCommands.ConsoleCommands,
      IvyConsole.Name :: Nil
    )
    StandardMain.runManaged(state)
  }
}

object StandardMain {
  private[sbt] lazy val exchange = new CommandExchange()
  // The access to the pool should be thread safe because lazy val instantiation is thread safe
  // and pool is only referenced directly in closeRunnable after the executionContext is sure
  // to have been instantiated
  private var pool: Option[ForkJoinPool] = None
  private[sbt] lazy val executionContext: ExecutionContext = ExecutionContext.fromExecutor({
    val p = new ForkJoinPool
    pool = Some(p)
    p
  })

  private val closeRunnable = () => {
    RunningProcesses.killAll()
    exchange.shutdown()
    pool.foreach(_.shutdownNow())
  }

  private val isShutdown = new AtomicBoolean(false)
  def runManaged(s: State): xsbti.MainResult = {
    val hook = ShutdownHooks.add(closeRunnable)
    try {
      MainLoop.runLogged(s)
    } catch {
      case _: InterruptedException if isShutdown.get =>
        new xsbti.Exit { override def code(): Int = 0 }
    } finally {
      try DefaultBackgroundJobService.shutdown()
      finally hook.close()
      ()
    }
  }

  /** The common interface to standard output, used for all built-in ConsoleLoggers. */
  val console: ConsoleOut =
    ConsoleOut.systemOutOverwrite(ConsoleOut.overwriteContaining("Resolving "))
  ConsoleOut.setGlobalProxy(console)

  private[sbt] def logLevelFromArguments(args: Seq[String]): Level.Value =
    val earlyCmd = BasicCommandStrings.EarlyCommand
    val levelOptions = Level.values.toSeq.flatMap: v =>
      List("-" + v.toString, "--" + v.toString)
    def levelFromArg(arg: String): Option[Level.Value] =
      if arg.startsWith(earlyCmd + "(") && arg.endsWith(")") then
        val inner = arg.slice(earlyCmd.length + 1, arg.length - 1).trim
        Level.values.find(_.toString == inner)
      else if levelOptions.contains(arg) then
        Level.values.find(v => arg == "-" + v.toString || arg == "--" + v.toString)
      else None
    args.flatMap(levelFromArg).headOption.getOrElse(Level.Info)

  private def initialGlobalLogging(file: Option[File], initialLevel: Level.Value): GlobalLogging =
    def createTemp(prefix: String)(attempt: Int = 0): File = Retry:
      file.foreach(f => if (!f.exists()) IO.createDirectory(f))
      File.createTempFile(prefix, ".log", file.orNull)

    // Call this experimental since we don't want to commit to the log format for now
    val execLogProp = "sbt.experimental_execution_log"
    val execLog =
      if java.lang.Boolean.getBoolean(execLogProp) then
        Option(ActionCache.setExecLog(createTemp("exec-log")().toPath()))
      else sys.props.get(execLogProp).map(Paths.get(_)).map(ActionCache.setExecLog)
    execLog.foreach: log =>
      ShutdownHooks.add(() => log.close())
    GlobalLogging.initial(
      MainAppender.globalDefault(ConsoleOut.globalProxy),
      createTemp("sbt-global-log")(),
      ConsoleOut.globalProxy,
      initialLevel
    )

  private def initialGlobalLogging(file: Option[File]): GlobalLogging =
    initialGlobalLogging(file, Level.Info)

  def initialGlobalLogging(file: File): GlobalLogging = initialGlobalLogging(Option(file))
  @deprecated("use version that takes file argument", "1.4.0")
  def initialGlobalLogging: GlobalLogging = initialGlobalLogging(None)

  def initialState(
      configuration: xsbti.AppConfiguration,
      initialDefinitions: Seq[Command],
      preCommands: Seq[String]
  ): State =
    if (!sys.props.contains("jna.nosys")) sys.props.put("jna.nosys", "true")

    import BasicCommandStrings.{ DashDashDetachStdio, DashDashServer, isEarlyCommand }
    val userCommands =
      configuration.arguments
        .map(_.trim)
        .filterNot(c => c == DashDashDetachStdio || c == DashDashServer)
        .toSeq
    val initialLevel = logLevelFromArguments(userCommands)
    val (earlyCommands, normalCommands) = (preCommands ++ userCommands).partition(isEarlyCommand)
    val commands = (earlyCommands ++ normalCommands).toList.map(x => Exec(x, None))
    val baseAttrs = BuiltinCommands.initialAttributes
    val initAttrs =
      if initialLevel == Level.Info then baseAttrs
      else
        baseAttrs
          .put(Keys.logLevel.key, initialLevel)
          .put(BasicKeys.explicitGlobalLogLevels, true)
    val logDir = BuildPaths.globalLoggingStandard(configuration.baseDirectory)
    val s = State(
      configuration,
      initialDefinitions,
      Set.empty,
      None,
      commands,
      State.newHistory,
      initAttrs,
      initialGlobalLogging(Option(logDir), initialLevel),
      None,
      State.Continue
    )
    s.initializeClassLoaderCache
  end initialState
}

import sbt.BasicCommandStrings.*
import sbt.BasicCommands.*
import sbt.CommandUtil.*
import sbt.TemplateCommandUtil.{ templateCommandAlias, templateCommand }
import sbt.internal.CommandStrings.*
import sbt.internal.util.complete.DefaultParsers.*

object BuiltinCommands {
  def initialAttributes = AttributeMap.empty
  import BasicCommands.exit

  def aliasWithKeyConflictCheck: Command =
    Command(AliasCommand, Help.more(AliasCommand, AliasDetailed)) { s =>
      val name = token(OpOrID.examples(aliasNames(s)*))
      val assign = token(OptSpace ~ '=' ~ OptSpace)
      val sfree = removeAliases(s)
      val to = matched(sfree.combinedParser, partial = true).failOnException | any.+.string
      OptSpace ~> (name ~ (assign ~> to.?).?).?
    }((s, args) => runAlias(s, args, Project.definedKeyNames))

  def ConsoleCommands: Seq[Command] =
    Seq(ignore, exit, IvyConsole.command, setLogLevel, early, act, nop)

  def ScriptCommands: Seq[Command] =
    Seq(ignore, exit, Script.command, setLogLevel, early, act, nop)

  @nowarn
  def DefaultCommands: Seq[Command] =
    Seq(
      multi,
      about,
      tasks,
      settingsCommand,
      loadProject,
      templateCommand,
      templateCommandAlias,
      projects,
      project,
      set,
      sessionCommand,
      inspect,
      loadProjectImpl,
      loadFailed,
      oldLoadFailed,
      Cross.crossBuild,
      Cross.switchVersion,
      CrossJava.switchJavaHome,
      CrossJava.crossJavaHome,
      PluginCross.pluginCross,
      PluginCross.pluginSwitch,
      Cross.crossRestoreSession,
      setLogLevel,
      plugin,
      plugins,
      addPluginSbtFile,
      writeSbtVersion,
      skipBanner,
      notifyUsersAboutShell,
      shell,
      rebootNetwork,
      startServer,
      eval,
      last,
      lastGrep,
      exportCommand,
      boot,
      initialize,
      act,
      continuous,
      clearCaches,
      Clean.cleanFull,
      NetworkChannel.disconnect,
      waitCmd,
      promptChannel,
      TestCommand.testOnly,
      aliasWithKeyConflictCheck,
    ) ++
      allBasicCommands.filterNot(_.nameOption.contains(AliasCommand)) ++
      ContinuousCommands.value ++
      BuildServerProtocol.commands

  def DefaultBootCommands: Seq[String] =
    WriteSbtVersion :: LoadProject :: NotifyUsersAboutShell :: s"$IfLast $Shell" :: Nil

  def boot = Command.make(BootCommand)(bootParser)

  def about = Command.command(AboutCommand, aboutBrief, aboutDetailed) { s =>
    s.log.info(aboutString(s)); s
  }

  def setLogLevel = Command.arb(const(logLevelParser), logLevelHelp)(LogManager.setGlobalLogLevel)
  private def logLevelParser: Parser[Level.Value] =
    oneOf(Level.values.toSeq.map(v => v.toString ^^^ v))

  // This parser schedules the default boot commands unless overridden by an alias
  def bootParser(s: State) = {
    val orElse = () => DefaultBootCommands.toList ::: s
    delegateToAlias(BootCommand, success(orElse))(s)
  }

  def sbtName(s: State): String = s.configuration.provider.id.name
  def sbtVersion(s: State): String = s.configuration.provider.id.version
  def scalaVersion(s: State): String = s.configuration.provider.scalaProvider.version
  def aboutProject(s: State): String =
    if (Project.isProjectLoaded(s)) {
      val e = Project.extract(s)
      val version = e.getOpt(Keys.version) match { case None => ""; case Some(v) => " " + v }
      val current = "The current project is " + Reference.display(e.currentRef) + version + "\n"
      val sc = aboutScala(s, e)
      val built = if (sc.isEmpty) "" else "The current project is built against " + sc + "\n"
      current + built + aboutPlugins(e)
    } else "No project is currently loaded"

  def aboutPlugins(e: Extracted): String = {
    def plugins(lbu: LoadedBuildUnit) =
      lbu.unit.plugins.detected.autoPlugins
        .map(_.value.label)

    val allPluginNames =
      e.structure.units.values
        .flatMap(plugins)
        .toList
        .distinct
        .sorted

    if (allPluginNames.isEmpty)
      ""
    else
      ("Available Plugins" +: allPluginNames)
        .mkString("\n - ")
  }

  def aboutScala(s: State, e: Extracted): String = {
    val scalaVersion = e.getOpt(Keys.scalaVersion)
    val scalaHome = e.getOpt(Keys.scalaHome).flatMap(idFun)
    val instance =
      e.getOpt(Keys.scalaInstance).flatMap(_ => quiet(e.runTask(Keys.scalaInstance, s)._2))
    (scalaVersion, scalaHome, instance) match {
      case (sv, Some(home), Some(si)) =>
        "local Scala version " + selectScalaVersion(sv, si) + " at " + home.getAbsolutePath
      case (_, Some(home), None)  => "a local Scala build at " + home.getAbsolutePath
      case (sv, None, Some(si))   => "Scala " + selectScalaVersion(sv, si)
      case (Some(sv), None, None) => "Scala " + sv
      case (None, None, None)     => ""
    }
  }

  def aboutString(s: State): String = {
    val (name, ver, scalaVer, about) =
      (sbtName(s), sbtVersion(s), scalaVersion(s), aboutProject(s))
    """This is %s %s
			|%s
			|%s, %s plugins, and build definitions are using Scala %s
			|""".stripMargin.format(name, ver, about, name, name, scalaVer)
  }

  private def selectScalaVersion(sv: Option[String], si: ScalaInstance): String =
    sv match {
      case Some(si.version) => si.version
      case _                => si.actualVersion
    }

  private def quiet[T](t: => T): Option[T] =
    try Some(t)
    catch case _: Exception => None

  def settingsCommand: Command =
    showSettingLike(
      SettingsCommand,
      settingsPreamble,
      KeyRanks.MainSettingCutoff,
      key => key.tag.isSetting
    )

  def tasks: Command =
    showSettingLike(
      TasksCommand,
      tasksPreamble,
      KeyRanks.MainTaskCutoff,
      key => key.tag.isTaskOrInputTask
    )

  def showSettingLike(
      command: String,
      preamble: String,
      cutoff: Int,
      keep: AttributeKey[?] => Boolean
  ): Command =
    Command(command, settingsBrief(command), settingsDetailed(command))(showSettingParser(keep)) {
      case (s: State, (verbosity: Int, selected: Option[String])) =>
        if (selected.isEmpty) System.out.println(preamble)
        val prominentOnly = verbosity <= 1
        val verboseFilter = if (prominentOnly) highPass(cutoff) else topNRanked(25 * verbosity)
        System.out.println(tasksHelp(s, keys => verboseFilter(keys filter keep), selected))
        System.out.println()
        if (prominentOnly) System.out.println(moreAvailableMessage(command, selected.isDefined))
        s
    }
  def showSettingParser(
      keepKeys: AttributeKey[?] => Boolean
  )(s: State): Parser[(Int, Option[String])] =
    verbosityParser ~ selectedParser(s, keepKeys).?
  def selectedParser(s: State, keepKeys: AttributeKey[?] => Boolean): Parser[String] =
    singleArgument(allTaskAndSettingKeys(s).withFilter(keepKeys).map(_.label).toSet)
  def verbosityParser: Parser[Int] =
    success(1) | ((Space ~ "-") ~> (
      'v'.id.+.map(_.size + 1) |
        ("V" ^^^ Int.MaxValue)
    ))

  def taskDetail(keys: Seq[AttributeKey[?]], firstOnly: Boolean): Seq[(String, String)] =
    sortByLabel(withDescription(keys)) flatMap { t =>
      taskStrings(t, firstOnly)
    }

  def taskDetail(keys: Seq[AttributeKey[?]]): Seq[(String, String)] =
    taskDetail(keys, false)

  def allTaskAndSettingKeys(s: State): Seq[AttributeKey[?]] = {
    val extracted = Project.extract(s)
    import extracted.*
    val index = structure.index
    index.keyIndex
      .keys(Some(currentRef))
      .toSeq
      .map { key =>
        try Some(index.keyMap(key))
        catch {
          case NonFatal(ex) =>
            s.log.debug(ex.getMessage)
            None
        }
      }
      .collect { case Some(s) => s }
      .distinct
  }

  def sortByLabel(keys: Seq[AttributeKey[?]]): Seq[AttributeKey[?]] = keys.sortBy(_.label)
  def sortByRank(keys: Seq[AttributeKey[?]]): Seq[AttributeKey[?]] = keys.sortBy(_.rank)
  def withDescription(keys: Seq[AttributeKey[?]]): Seq[AttributeKey[?]] =
    keys.filter(_.description.isDefined)

  def topNRanked(n: Int) = (keys: Seq[AttributeKey[?]]) => sortByRank(keys).take(n)

  def highPass(rankCutoff: Int) =
    (keys: Seq[AttributeKey[?]]) => sortByRank(keys).takeWhile(_.rank <= rankCutoff)

  def tasksHelp(
      s: State,
      filter: Seq[AttributeKey[?]] => Seq[AttributeKey[?]],
      arg: Option[String]
  ): String = {
    val commandAndDescription = taskDetail(filter(allTaskAndSettingKeys(s)), true)
    arg match {
      case Some(selected) => detail(selected, commandAndDescription.toMap)
      case None           => aligned("  ", "   ", commandAndDescription) mkString ("\n", "\n", "")
    }
  }

  def taskStrings(key: AttributeKey[?], firstOnly: Boolean): Option[(String, String)] =
    key.description map { d =>
      if (firstOnly) (key.label, d.split("\r?\n")(0)) else (key.label, d)
    }

  def taskStrings(key: AttributeKey[?]): Option[(String, String)] = taskStrings(key, false)

  def defaults = Command.command(DefaultsCommand) { s =>
    s.copy(definedCommands = DefaultCommands)
  }

  def initialize: Command = Command.command(InitCommand) { s =>
    /*"load-commands -base ~/.sbt/commands" :: */
    readLines(readable(sbtRCs(s))).toList ::: s
  }

  def eval: Command = Command.single(EvalCommand, Help.more(EvalCommand, evalDetailed)) {
    (s, arg) =>
      if (Project.isProjectLoaded(s)) loadedEval(s, arg) else rawEval(s, arg)
      s
  }

  def continuous: Command = Continuous.continuous

  private def loadedEval(s: State, arg: String): Unit = {
    val extracted = Project.extract(s)
    import extracted.*
    val result =
      session.currentEval().evalInfer(expression = arg, imports = autoImports(extracted))
    s.log.info(s"ans: ${result.tpe} = ${result.getValue(currentLoader)}")
  }

  private def rawEval(s: State, arg: String): Unit = {
    val app = s.configuration.provider
    val classpath = app.mainClasspath ++ app.scalaProvider.jars
    val result = Load
      .mkEval(classpath.map(_.toPath()).toSeq, s.baseDir, Nil)
      .evalInfer(expression = arg, imports = EvalImports(Nil))
    s.log.info(s"ans: ${result.tpe} = ${result.getValue(app.loader)}")
  }

  def sessionCommand: Command =
    Command.make(SessionCommand, sessionBrief, SessionSettings.Help)(SessionSettings.command)

  def reapply(newSession: SessionSettings, structure: BuildStructure, s: State): State = {
    s.log.info("Reapplying settings...")
    // For correct behavior, we also need to re-inject a settings logger, as we'll be re-evaluating settings
    val loggerInject = LogManager.settingsLogger(s)
    val withLogger = newSession.appendRaw(loggerInject :: Nil)
    val show = Project.showContextKey2(newSession)
    val newStructure = Load.reapply(withLogger.mergeSettings, structure)(using show)
    Project.setProject(newSession, newStructure, s)
  }

  def set: Command = Command(SetCommand, setBrief, setDetailed)(setParser) { case (s, (all, arg)) =>
    val extracted = Project.extract(s)
    import extracted.*
    val dslVals = extracted.currentUnit.unit.definitions.dslDefinitions
    // TODO - This is possibly inefficient (or stupid).  We should try to only attach the
    // classloader + imports NEEDED to compile the set command, rather than
    // just ALL of them.
    val ims = (imports(extracted) ++ dslVals.imports.map(i => (i, -1)))
    val cl = dslVals.classloader(currentLoader)
    val settings = EvaluateConfigurations.evaluateSetting(
      session.currentEval(),
      "<set>",
      ims,
      arg,
      LineRange(0, 0)
    )(cl)
    val setResult =
      if (all) SettingCompletions.setAll(extracted, settings)
      else SettingCompletions.setThis(extracted, settings, arg)
    s.log.info(setResult.quietSummary)
    s.log.debug(setResult.verboseSummary)
    reapply(setResult.session, structure, s)
  }

  @deprecated("Use variant that doesn't take a State", "1.1.1")
  def setThis(
      s: State,
      extracted: Extracted,
      settings: Seq[Def.Setting[?]],
      arg: String
  ): SetResult =
    setThis(extracted, settings, arg)

  def setThis(
      extracted: Extracted,
      settings: Seq[Def.Setting[?]],
      arg: String
  ): SetResult =
    SettingCompletions.setThis(extracted, settings, arg)

  def inspect: Command = Command(InspectCommand, inspectBrief, inspectDetailed)(Inspect.parser) {
    (s, f) =>
      s.log.info(f())
      s
  }

  def lastGrep: Command =
    lastGrepCommand(LastGrepCommand, lastGrepBrief, lastGrepDetailed, lastGrepParser)

  private def lastGrepCommand(
      name: String,
      briefHelp: (String, String),
      detail: String,
      parser: State => Parser[(String, Option[AnyKeys])]
  ): Command =
    Command(name, briefHelp, detail)(parser) { (s: State, sks: (String, Option[AnyKeys])) =>
      {
        (s, sks) match {
          case (s, (pattern, Some(sks))) =>
            val (str, _, display) = extractLast(s)
            Output.lastGrep(sks, str.streams(s), pattern, printLast)(using display)
            keepLastLog(s)
          case (s, (pattern, None)) =>
            for (logFile <- lastLogFile(s)) yield Output.lastGrep(logFile, pattern, printLast)
            keepLastLog(s)
        }
      }
    }

  def extractLast(s: State): (BuildStructure, Select[ProjectRef], Show[Def.ScopedKey[?]]) = {
    val ext = Project.extract(s)
    (ext.structure, Select(ext.currentRef), ext.showKey)
  }

  def setParser = (s: State) => {
    val extracted = Project.extract(s)
    import extracted.*
    token(Space ~> flag("every" ~ Space)) ~
      SettingCompletions.settingParser(structure.data, structure.index.keyMap, currentProject)
  }

  import Def.ScopedKey
  // type PolyStateKeysParser = [a] => State => Parser[Seq[ScopedKey[a]]]
  type KeysParser = Parser[Seq[ScopedKey[Any]]]

  val spacedAggregatedParser: State => KeysParser = (s: State) =>
    Act.requireSession(s, token(Space) ~> Act.aggregatedKeyParser(s))

  val aggregatedKeyValueParser: State => Parser[Option[AnyKeys]] = (s: State) =>
    spacedAggregatedParser(s).map(x => Act.keyValues(s)(x)).?

  val exportParser: State => Parser[() => State] = (s: State) =>
    Act.requireSession(s, token(Space) ~> exportParser0(s))

  private[sbt] def exportParser0(s: State): Parser[() => State] = {
    val extracted = Project.extract(s)
    import extracted.{ showKey, structure }
    val keysParser = token(flag("--last" <~ Space)) ~ Act.aggregatedKeyParser(extracted)
    val show = Aggregation.ShowConfig(
      settingValues = true,
      taskValues = false,
      print = println(_),
      success = false
    )
    for {
      lastOnly_keys <- keysParser
      kvs = Act.keyValues(structure)(lastOnly_keys._2)
      f <-
        if (lastOnly_keys._1) success(() => s)
        else Aggregation.evaluatingParser(s, show)(kvs)
    } yield () => {
      def export0(s: State): State = lastImpl(s, kvs, Some(ExportStream))
      val newS =
        try f()
        catch {
          case NonFatal(e) =>
            try export0(s)
            finally {
              throw e
            }
        }
      export0(newS)
    }
  }

  def lastGrepParser(s: State): Parser[(String, Option[AnyKeys])] =
    Act.requireSession(
      s,
      (token(Space) ~> token(NotSpace, "<pattern>")) ~ aggregatedKeyValueParser(s)
    )

  def last: Command = Command(LastCommand, lastBrief, lastDetailed)(aggregatedKeyValueParser) {
    case (s, Some(sks)) => lastImpl(s, sks, None)
    case (s, None)      =>
      for (logFile <- lastLogFile(s)) yield Output.last(logFile, printLast)
      keepLastLog(s)
  }

  def exportCommand: Command =
    Command(ExportCommand, exportBrief, exportDetailed)(exportParser)((_, f) => f())

  private def lastImpl(s: State, sks: AnyKeys, sid: Option[String]): State = {
    val (str, _, display) = extractLast(s)
    Output.last(sks, str.streams(s), printLast, sid)(using display)
    keepLastLog(s)
  }

  /** Determines the log file that last* commands should operate on.  See also isLastOnly. */
  def lastLogFile(s: State): Option[File] = {
    val backing = s.globalLogging.backing
    if (isLastOnly(s)) backing.last else Some(backing.file)
  }

  /**
   * If false, shift the current log file to be the log file that 'last' will operate on.
   * If true, keep the previous log file as the one 'last' operates on because there is nothing useful in the current one.
   */
  def keepLastLog(s: State): State = if (isLastOnly(s)) s.keepLastLog else s

  /**
   * The last* commands need to determine whether to read from the current log file or the previous log file
   * and whether to keep the previous log file or not.  This is selected based on whether the previous command
   * was 'shell', which meant that the user directly entered the 'last' command.  If it wasn't directly entered,
   * the last* commands operate on any output since the last 'shell' command and do shift the log file.
   * Otherwise, the output since the previous 'shell' command is used and the log file is not shifted.
   */
  def isLastOnly(s: State): Boolean = s.history.previous.forall(_.commandLine == Shell)

  def printLast: Seq[String] => Unit = _ foreach println

  def autoImports(extracted: Extracted): EvalImports =
    new EvalImports(imports(extracted).map(_._1)) // <auto-imports>

  def imports(extracted: Extracted): Seq[(String, Int)] = {
    val curi = extracted.currentRef.build
    extracted.structure.units(curi).imports.map(s => (s, -1))
  }

  def listBuild(
      uri: URI,
      build: LoadedBuildUnit,
      current: Boolean,
      currentID: String,
      log: Logger
  ): Unit = {
    log.info(s"In $uri")
    def prefix(id: String) = if (currentID != id) "   " else if (current) " * " else "(*)"
    for (id <- build.defined.keys.toSeq.sorted) log.info("\t" + prefix(id) + id)
  }

  def act: Command = Command.customHelp(Act.actParser, actHelp)

  def actHelp: State => Help = { s =>
    CommandStrings.showHelp ++ CommandStrings.printHelp ++ CommandStrings.multiTaskHelp ++
      keysHelp(s)
  }

  def keysHelp(s: State): Help =
    if (Project.isProjectLoaded(s))
      Help.detailOnly(taskDetail(allTaskAndSettingKeys(s)))
    else
      Help.empty

  def plugins: Command = Command.command(PluginsCommand, pluginsBrief, pluginsDetailed) { s =>
    val helpString = PluginsDebug.helpAll(s)
    System.out.println(helpString)
    s
  }

  val pluginParser: State => Parser[AutoPlugin] = s => {
    val autoPlugins: Map[String, AutoPlugin] = PluginsDebug.autoPluginMap(s)
    token(Space) ~> Act.knownPluginParser(autoPlugins, "plugin")
  }

  def plugin: Command = Command(PluginCommand)(pluginParser) { (s, plugin) =>
    val helpString = PluginsDebug.help(plugin, s)
    System.out.println(helpString)
    s
  }

  def projects: Command =
    Command(ProjectsCommand, (ProjectsCommand, projectsBrief), projectsDetailed)(s =>
      projectsParser(s).?
    ) {
      case (s, Some(modifyBuilds)) => transformExtraBuilds(s, modifyBuilds)
      case (s, None)               => showProjects(s); s
    }

  def showProjects(s: State): Unit = {
    val extracted = Project.extract(s)
    import extracted.*
    listBuild(currentRef.build, structure.units(currentRef.build), true, currentRef.project, s.log)
    for ((uri, build) <- structure.units if currentRef.build != uri)
      listBuild(uri, build, false, currentRef.project, s.log)
  }

  def transformExtraBuilds(s: State, f: List[URI] => List[URI]): State = {
    val original = Project.extraBuilds(s)
    val extraUpdated = Project.updateExtraBuilds(s, f)
    try doLoadProject(extraUpdated, LoadAction.Current)
    catch {
      case _: Exception =>
        s.log.error("Project loading failed: reverting to previous state.")
        Project.setExtraBuilds(s, original)
    }
  }

  def projectsParser(s: State): Parser[List[URI] => List[URI]] = {
    val addBase = token(Space ~> "add") ~> token(Space ~> basicUri, "<build URI>").+
    val removeBase = token(Space ~> "remove") ~> token(Space ~> Uri(Project.extraBuilds(s).toSet)).+
    addBase.map(toAdd => (xs: List[URI]) => (toAdd.toList ::: xs).distinct) |
      removeBase.map(toRemove => (xs: List[URI]) => xs.filterNot(toRemove.toSet))
  }

  def project: Command =
    Command.make(ProjectCommand, projectBrief, projectDetailed)(ProjectNavigation.command)

  def loadFailed: Command = Command(LoadFailed)(loadProjectParser)(doLoadFailed)
  @deprecated("Use `loadFailed` instead.", "1.2.0")
  def oldLoadFailed: Command =
    Command(OldLoadFailed) { s =>
      loadProjectParser(s)
    } { (s: State, loadArg: String) =>
      s.log.warn(
        deprecationWarningText(OldLoadFailed, LoadFailed)
      )
      doLoadFailed(s, loadArg)
    }

  private def deprecationWarningText(oldCommand: String, newCommand: String) = {
    s"The `$oldCommand` command is deprecated in favor of `$newCommand` and will be removed in a later version"
  }

  @tailrec
  private def doLoadFailed(s: State, loadArg: String): State = {
    // In batch mode, exit with failure to avoid infinite retry loops on persistent errors
    if (ITerminal.console.prompt == Prompt.Batch) {
      s.exit(ok = false)
    } else {
      s.log.warn("Project loading failed: (r)etry, (q)uit, (l)ast, or (i)gnore? (default: r)")
      val result: Int =
        try
          ITerminal.get.withRawInput(System.in.read) match {
            case -1 => 'q'.toInt
            case b  => b
          }
        catch { case _: ClosedChannelException => 'q' }
      def retry: State = loadProjectCommand(LoadProject, loadArg) :: s.clearGlobalLog
      def ignoreMsg: String =
        if (Project.isProjectLoaded(s)) "using previously loaded project" else "no project loaded"

      result.toChar match {
        case '\n' | '\r' => retry
        case 'r' | 'R'   => retry
        case 'q' | 'Q'   => s.exit(ok = false)
        case 'i' | 'I'   => s.log.warn(s"Ignoring load failure: $ignoreMsg."); s
        case 'l' | 'L'   => LastCommand :: loadProjectCommand(LoadFailed, loadArg) :: s
        case c           => println(s"Invalid response: '$c'"); doLoadFailed(s, loadArg)
      }
    }
  }

  def loadProjectCommands(arg: String): List[String] =
    StashOnFailure ::
      (OnFailure + " " + loadProjectCommand(LoadFailed, arg)) ::
      loadProjectCommand(LoadProjectImpl, arg) ::
      State.FailureWall ::
      PopOnFailure ::
      Nil

  def loadProject: Command =
    Command(LoadProject, LoadProjectBrief, LoadProjectDetailed)(loadProjectParser)((s, arg) =>
      loadProjectCommands(arg) ::: s
    )

  private def loadProjectParser: State => Parser[String] =
    _ => matched(Project.loadActionParser)

  private def loadProjectCommand(command: String, arg: String): String =
    s"$command $arg".trim

  def loadProjectImpl: Command =
    Command(LoadProjectImpl)(_ => Project.loadActionParser)(doLoadProject)

  def checkSBTVersionChanged(state: State): Unit = {
    import sbt.io.syntax.*
    val sbtVersionProperty = "sbt.version"

    // Don't warn if current version has been set in system properties
    val sbtVersionSystemOpt =
      Option(System.getProperty(sbtVersionProperty))

    // the intention is to warn if build.properties file has changed during current sbt session
    // a `reload` will not respect this change while `reboot` will.
    val buildProps = state.baseDir / "project" / "build.properties"
    val sbtVersionBuildOpt = if (buildProps.exists) {
      val buildProperties = new Properties()
      IO.load(buildProperties, buildProps)
      Option(buildProperties.getProperty(sbtVersionProperty)).map(_.trim)
    } else None

    val sbtVersionOpt = sbtVersionSystemOpt.orElse(sbtVersionBuildOpt)

    sbtVersionOpt.foreach { version =>
      val appVersion = state.configuration.provider.id.version()
      if (version != appVersion) {
        state.log.warn(
          s"sbt version mismatch, using: $appVersion, " +
            s"""in build.properties: "$version", use 'reboot' to use the new value.""".stripMargin
        )
      }
    }
  }

  private def welcomeBanner(state: State): Unit = {
    import scala.util.Properties
    val appVersion = state.configuration.provider.id.version()
    val javaVersion = s"${Properties.javaVendor} Java ${Properties.javaVersion}"
    state.log.info(s"welcome to sbt $appVersion ($javaVersion)")
  }

  def doLoadProject(s0: State, action: LoadAction): State = {
    welcomeBanner(s0)
    checkSBTVersionChanged(s0)
    val (s1, base) = Project.loadAction(SessionVar.clear(s0), action)
    IO.createDirectory(base)
    val s2 = if (s1 has Keys.stateCompilerCache) s1 else registerCompilerCache(s1)
    val (eval, structure) =
      try Load.defaultLoad(s2, base, s2.log, Project.inPluginProject(s2), Project.extraBuilds(s2))
      catch {
        case ex: sbt.internal.EvalException =>
          s0.log.debug(ex.getMessage)
          ex.getStackTrace map (ste => s"\tat $ste") foreach (s0.log.debug(_))
          ex.setStackTrace(Array.empty)
          throw ex
      }

    val session = Load.initialSession(structure, eval, s0)
    SessionSettings.checkSession(session, s2)
    val s3 = Project.setProject(
      session,
      structure,
      s2,
      st => setupGlobalFileTreeRepository(Clean.addCacheStoreFactoryFactory(st))
    )
    addSuperShellParams(
      CheckBuildSources.init(
        LintUnused.lintScalaVersion(LintUnused.lintUnusedFunc(s3))
      )
    )
  }

  private val setupGlobalFileTreeRepository: State => State = { state =>
    state.get(sbt.nio.Keys.globalFileTreeRepository).foreach(_.close())
    state.put(sbt.nio.Keys.globalFileTreeRepository, FileTreeRepository.default)
  }
  private val addSuperShellParams: State => State = (s: State) => {
    val extracted = Project.extract(s)
    import scala.concurrent.duration.*
    val sleep = extracted.getOpt(Keys.superShellSleep).getOrElse(SysProp.supershellSleep.millis)
    val threshold =
      extracted.getOpt(Keys.superShellThreshold).getOrElse(SysProp.supershellThreshold)
    val maxItems = extracted.getOpt(Keys.superShellMaxTasks).getOrElse(SysProp.supershellMaxTasks)
    ITerminal.setConsoleProgressState(new ProgressState(1, maxItems))
    s.put(Keys.superShellSleep.key, sleep)
      .put(Keys.superShellThreshold.key, threshold)
      .put(Keys.superShellMaxTasks.key, maxItems)
  }

  def registerCompilerCache(s: State): State = Clean.registerCompilerCache(s)
  def clearCaches: Command = Clean.clearCaches

  private[sbt] def waitCmd: Command =
    Command.arb(_ =>
      ContinuousCommands.waitWatch.examples() ~> " ".examples() ~> matched(any.*).examples()
    ) { (s0, channel) =>
      val exchange = StandardMain.exchange
      exchange.channelForName(channel) match {
        case Some(c) if ContinuousCommands.isInWatch(s0, c) =>
          if (c.terminal.prompt != Prompt.Watch) {
            c.terminal.setPrompt(Prompt.Watch)
            c.prompt(ConsolePromptEvent(s0))
          } else if (c.terminal.isSupershellEnabled) {
            c.terminal.printStream.print(ConsoleAppender.ClearScreenAfterCursor)
            c.terminal.printStream.flush()
          }

          val s1 = exchange.run(s0)
          val exec: Exec = getExec(s1, Duration.Inf)
          val wait = s"${ContinuousCommands.waitWatch} $channel"
          val onFailure =
            s1.onFailure.map(of => if (of.commandLine == Shell) of.withCommandLine(wait) else of)
          val waitExec = Exec(wait, None)
          val remaining: List[Exec] = Exec(FailureWall, None) :: waitExec :: s1.remainingCommands
          val newState = s1.copy(remainingCommands = exec +: remaining, onFailure = onFailure)
          if (exec.commandLine.trim.isEmpty) newState
          else newState.clearGlobalLog
        case _ => s0
      }
    }

  private[sbt] def promptChannel = Command.arb(_ => reportParser(PromptChannel)) {
    (state, channel) =>
      if (channel == ConsoleChannel.defaultName) {
        if (!state.remainingCommands.exists(_.commandLine == Shell))
          state.copy(remainingCommands = state.remainingCommands ::: (Exec(Shell, None) :: Nil))
        else state
      } else {
        StandardMain.exchange.channelForName(channel) match {
          case Some(nc: NetworkChannel) => nc.prompt()
          case _                        =>
        }
        state
      }
  }

  private def getExec(state: State, interval: Duration): Exec = {
    StandardMain.exchange.blockUntilNextExec(interval, Some(state), state.globalLogging.full)
  }

  def shell: Command = Command.command(Shell, Help.more(Shell, ShellDetailed)) { s0 =>
    import sbt.internal.ConsolePromptEvent
    val exchange = StandardMain.exchange
    val welcomeState = displayWelcomeBanner(s0)
    val s1 = exchange.run(welcomeState)
    /*
     * It is possible for sbt processes to leak if two are started simultaneously
     * by a remote client and only one is able to start a server. This seems to
     * happen primarily on windows.
     */
    if (ITerminal.startedByRemoteClient && !exchange.hasServer) {
      Exec(Shutdown, None) +: s1
    } else {
      if (ITerminal.console.prompt == Prompt.Batch) ITerminal.console.setPrompt(Prompt.Pending)
      exchange.prompt(ConsolePromptEvent(s0))
      val minGCInterval = Project
        .extract(s1)
        .getOpt(Keys.minForcegcInterval)
        .getOrElse(GCUtil.defaultMinForcegcInterval)
      val exec: Exec = getExec(s1, minGCInterval)
      val isInteractive = exec.source match {
        case Some(src) if src.channelName.startsWith("network") =>
          exchange.channelForName(src.channelName) match {
            case Some(nc: NetworkChannel) => nc.isInteractive
            case _                        => true
          }
        case _ => true
      }
      val newState = s1
        .copy(
          onFailure = Some(Exec(Shell, None)),
          remainingCommands = exec +: Exec(Shell, None) +: s1.remainingCommands
        )
        .setInteractive(isInteractive)
      val res =
        if (exec.commandLine.trim.isEmpty) newState
        else newState.clearGlobalLog
      res
    }
  }

  def rebootNetwork: Command = Command.arb(_ => (RebootNetwork: Parser[String]).examples()) {
    (s, _) =>
      StandardMain.exchange.reboot(s)
      s
  }
  def startServer: Command =
    Command.command(StartServer, Help.more(StartServer, StartServerDetailed)) { s0 =>
      val exchange = StandardMain.exchange
      exchange.runServer(s0)
    }

  private val sbtVersionRegex = """sbt\.version\s*=.*""".r
  private def isSbtVersionLine(s: String) = sbtVersionRegex.pattern.matcher(s).matches()

  /** True if baseDir appears to be inside a scripted test directory (sbt#4536). */
  private def isScriptedTestDirectory(baseDir: File): Boolean = {
    val path = baseDir.getAbsolutePath
    path.contains(java.io.File.separator + "sbt-test" + java.io.File.separator) ||
    path.contains("/sbt-test/")
  }

  private def writeSbtVersionUnconditionally(state: State): Unit = {
    val baseDir = state.baseDir
    if (isScriptedTestDirectory(baseDir)) {
      // sbt#4536: don't alter scripted test setup
    } else {
      val sbtVersion = BuiltinCommands.sbtVersion(state)
      val projectDir = baseDir / "project"
      val buildProps = projectDir / "build.properties"

      val buildPropsLines = if (buildProps.canRead) IO.readLines(buildProps) else Nil

      val sbtVersionAbsent = buildPropsLines forall (!isSbtVersionLine(_))

      if (sbtVersionAbsent) {
        val warnMsg = s"No sbt.version set in project/build.properties, base directory: $baseDir"
        try {
          if (isSbtBuild(baseDir)) {
            val line = s"sbt.version=$sbtVersion"
            IO.writeLines(buildProps, line :: buildPropsLines)
            state.log.info(s"Updated file $buildProps: set sbt.version to $sbtVersion")
          } else state.log.warn(warnMsg)
        } catch {
          case _: IOException => state.log.warn(warnMsg)
        }
      }
    }
  }

  private def intendsToInvokeNew(state: State) =
    state.remainingCommands exists (_.commandLine == TemplateCommand)

  private def writeSbtVersion(state: State) =
    if (SysProp.genBuildProps && !intendsToInvokeNew(state)) {
      writeSbtVersionUnconditionally(state)
    }

  private def checkRoot(state: State): Unit =
    if (SysProp.allowRootDir) ()
    else {
      val baseDir = state.baseDir
      import scala.jdk.CollectionConverters.*
      // this should return / on Unix and C:\ for Windows.
      val rootOpt = FileSystems.getDefault.getRootDirectories.asScala.toList.headOption
      rootOpt foreach { root =>
        if (baseDir.getAbsolutePath == root.toString) {
          throw new IllegalStateException(
            "cannot run sbt from root directory without -Dsbt.rootdir=true; see sbt/sbt#1458"
          )
        }
      }
    }

  private def WriteSbtVersion = "writeSbtVersion"

  private def writeSbtVersion: Command =
    Command.command(WriteSbtVersion) { state =>
      checkRoot(state)
      writeSbtVersion(state)
      state
    }

  private def intendsToInvokeCompile(state: State) =
    state.remainingCommands exists (_.commandLine == Keys.compile.key.label)
  private def hasRebooted(state: State) =
    state.remainingCommands exists (_.commandLine == StartServer)

  private def notifyUsersAboutShell(state: State): Unit = {
    val suppress =
      Project
        .extract(state)
        .getOpt(Keys.suppressSbtShellNotification)
        .getOrElse(false)
    if (!suppress && intendsToInvokeCompile(state) && !hasRebooted(state))
      state.log.info("Executing in batch mode. For better performance use sbt's shell")
  }

  private def NotifyUsersAboutShell = "notifyUsersAboutShell"

  private def notifyUsersAboutShell: Command =
    Command.command(NotifyUsersAboutShell) { state =>
      notifyUsersAboutShell(state); state
    }

  private def skipWelcomeFile(state: State, version: String) = {
    val base = BuildPaths.getGlobalBase(state).toPath
    base.resolve("preferences").resolve(version).resolve(SkipBannerFileName)
  }
  private def displayWelcomeBanner(state: State): State = {
    if (!state.get(bannerHasBeenShown).getOrElse(false)) {
      try {
        val version = sbtVersion(state)
        val skipFile = skipWelcomeFile(state, version)
        IO.createDirectory(skipFile.getParent.toFile())
        val suppress = !SysProp.banner || Files.exists(skipFile)
        if (!suppress) {
          Banner(version).foreach(banner => state.log.info(banner))
        }
      } catch {
        case _: IOException => /* Don't let errors in this command prevent startup */
      }
      state.put(bannerHasBeenShown, true)
    } else state
  }
  private val bannerHasBeenShown =
    AttributeKey[Boolean]("banner-has-been-shown", Int.MaxValue)
  private val SkipBannerFileName = "skip-banner"
  private val SkipBanner = "skipBanner"
  private def skipBanner: Command = Command.command(SkipBanner)(skipBanner)
  private def skipBanner(state: State): State = {
    val skipFile = skipWelcomeFile(state, sbtVersion(state))
    try Files.createFile(skipFile)
    catch {
      case _: FileAlreadyExistsException =>
      case e: IOException                => state.log.error(s"Couldn't create file $skipFile: $e")
    }
    state
  }
}
