Class DevCliInstaller

java.lang.Object
com.vaadin.flow.plugin.base.DevCliInstaller

public final class DevCliInstaller extends Object
Installs the vaadin-dev CLI and its agent skills into a project.

The payload ships as classpath resources of this module - the one that already holds logic shared by the Maven and Gradle plugins - so a future Gradle task reads the same bytes from the same jar. Nothing generated and no jars are copied: the CLI resolves the dev-loop daemon from the project's own dependencies at first use.

provisionHotswapAgent(java.nio.file.Path, com.vaadin.flow.plugin.base.PluginAdapterBase) is the second half of an install, and the only part that needs the network: it puts the one asset the loop cannot resolve from a Maven repository into the machine-level cache, so that a machine prepared here - a container image built while it had network access, say - can run the loop later with none. It runs the provisioning code out of the daemon jar the project resolves, which is deliberately not a dependency of this module; see there for why.

Everything is installed relative to one directory, and the two skill trees are installed as siblings, because the Claude adapter reaches the shared reference by relative path.

All of it is meant to be committed by the developer: it is project tooling, like mvnw, and the point is that every agent and every developer on the repository gets the same instructions.

For internal use only. May be renamed or removed in a future release.

  • Method Details

    • install

      public static DevCliInstaller.Result install(Path targetDirectory) throws IOException
      Installs the CLI and the skills under the given directory.

      The CLI goes into .vaadin/, the directory Vaadin already owns in a project: the dev-loop daemon writes its handshake there, so keeping the scripts beside it means one hidden directory rather than a second visible one at the project root.

      A file is written whenever it is absent or differs from the shipped copy, and left alone when it already holds exactly those bytes. There is deliberately no "you edited this, so I will keep it" case: these files are project tooling, like mvnw, and regenerating them is what the goal is for. Anyone who needs project-specific agent instructions should add their own skill beside these rather than edit them, and the goal is unbound, so it only ever runs when someone asks for it.

      That also makes it self-healing. A checkout is free to hand a committed text file back with the platform's line endings, and a vaadin-dev whose shebang ends \r\n does not run on Linux or macOS; because any difference is replaced, the next run repairs it.

      Parameters:
      targetDirectory - the directory to install into, normally the application module's base directory
      Returns:
      what was written and what was already up to date
      Throws:
      IOException - if a file cannot be read or written
    • provisionHotswapAgent

      public static Optional<Path> provisionHotswapAgent(Path daemonJar, PluginAdapterBase adapter) throws IOException
      Provisions the HotswapAgent jar into the machine-level dev-loop cache, which is the one thing the loop would otherwise download the first time someone runs vaadin-dev start.

      Doing it here rather than there is what makes the loop usable on a machine that has no network access by the time anyone develops on it. The daemon still provisions on demand, through the same code and into the same cache, so this only ever moves the download earlier - it never becomes a second copy or a second version.

      Nothing is written into the project: the cache is under ~/.vaadin/devloop, so one download serves every application on the machine and a mvn clean does not throw it away.

      The work is done by HotswapAgentJar inside daemonJar, reached through a throwaway class loader rather than by depending on the daemon from here. Two reasons, and the second is the one that decides it. A plugin dependency is resolved into the plugin realm before any goal runs, so every build - -Pproduction included, and every Gradle buildscript - would fetch dev-loop tooling nothing in it can call. And the version that matters is the one the project resolves: the CLI runs that daemon, so pre-downloading whatever version another copy of the daemon pins would leave the running daemon asking for a different one and reaching for the network anyway - in exactly the air-gapped container this feature exists for. Reading the pin out of the jar that will run makes the two agree by construction.

      Reflection is safe here because the daemon is dependency-free, by an enforcer rule in its own module: the platform class loader is a sufficient parent, and using that rather than this module loader also means nothing already in the plugin realm can shadow what is loaded.

      Parameters:
      daemonJar - the flow-devloop-daemon jar the project resolves
      adapter - the plugin adapter to report through
      Returns:
      the provisioned jar, or empty when that daemon is too old to carry the provisioning code - in which case a warning has been reported and the first start downloads as before
      Throws:
      IOException - if provisioning was attempted and the jar could neither be found in the cache nor downloaded
    • report

      public static void report(DevCliInstaller.Result result, Path targetDirectory, PluginAdapterBase adapter)
      Reports what an installation did, in the order a reader wants it: what changed, then what was left alone and why.

      Through the plugin adapter rather than a logger of its own, so the output lands in the build log the developer is already reading - the same route BuildFrontendUtil uses.

      Parameters:
      result - the installation result
      targetDirectory - the directory installed into, for relative reporting
      adapter - the plugin adapter to report through