For a long time, the examples module in Office-stamper served two masters: it was both a playground for users to see how to use the library and the source of our command-line interface. As the project grew, this “cramped” arrangement started to leak. Engine-specific concerns were mixing with CLI-specific dependencies like picocli, and it was becoming too easy to accidentally depend on internal core classes that were never meant to be exported.
This month, I finally made the split. By promoting the CLI to its own dedicated module, we’ve created a clean boundary that protects the engine’s purity while providing a robust, self-sufficient tool for our users.
Why now: Enforcing the Boundary
The primary driver for this change was encapsulation. I wanted a dedicated space where I could tell people to look at how an Office-stamper client is implemented without the risk of them (or me) importing non-exported core classes.
By moving to a separate office-stamper-cli module, we’ve turned the library’s consumer experience into a “canary.” If I change an interface in the engine, and the CLI breaks, I know immediately that I’ve violated a contract. This provides a much tighter feedback loop than the old examples package did.
What changed
- Module Promotion: Deleted the
examplesmodule and created a first-classclimodule. - Native Distribution: Integrated the
jpackage-maven-pluginto produce native executables (EXEs). - Toolchain Hygiene: Cleaned up the
pom.xmlstructure, replacing hardcoded strings with${project.*}variables and removing redundant plugin configurations. - Built-in Diagnostics: Migrated the environment-capturing
Diagnostictool into the CLI as a standard feature.
Implementation: Native Distribution with jpackage
One of the most exciting additions is the use of jpackage. I want the tool to be self-sufficient; a user shouldn’t have to worry about their Java installation just to run a diagnostic report or stamp a document.
<plugin>
<groupId>com.github.akman</groupId>
<artifactId>jpackage-maven-plugin</artifactId>
<version>0.1.5</version>
<configuration>
<name>${project.artifactId}</name>
<appversion>${project.version}</appversion>
<copyright>${copyright}</copyright>
<vendor>${vendor.name}</vendor>
<mainjar>${project.artifactId}-${project.version}-jar-with-dependencies.jar</mainjar>
<mainclass>pro.verron.officestamper.Main</mainclass>
<type>EXE</type>
<input>target/</input>
</configuration>
</plugin>
This ensures that the “try-out” experience is as frictionless as possible: download an EXE, point it at a template, and go.
The “Boring” Hygiene: Asynchronous Communication
During this refactor, I spent significant time on “boring” POM cleanup. To some, this might look like bike-shedding, but for a Solo Maintainer, it is essential.
I view build warnings as a form of ultimate asynchronous communication. A warning in the IDE or the build log is a prompt from the toolchain authors letting me know there’s a topic I’m not yet aware of—perhaps a future change I should anticipate, or a better pattern I should adopt. By keeping the build “clean-as-you-go,” I reduce the noise and allow these “lessons” to stand out when they appear.
Impact
- Zero-Install Trial: Users can now run Office-stamper as a native tool, lowering the barrier to entry for non-Java developers.
- Reliable Diagnostics: The bundled
Diagnostictemplate and data collector act as a reliable “smoke test” for the entire pipeline. - Clearer Documentation: The CLI code now serves as the canonical example of how to implement a client, using only the public API of the engine.
Next Steps
Now that the CLI is its own entity, I plan to:
- Expand the
jpackageconfiguration to support macOS and Linux installers. - Add more built-in “keyword” templates beyond the
diagnosticone to showcase complex features like Excel data sources. - Implement a plugin architecture for the CLI to allow users to add their own custom resolvers without re-compiling the core.
Checklist: CLI Best Practices
- Separate Concerns: Keep the CLI skin (parsing, I/O) separate from the engine logic.
- Native First: Provide self-contained binaries to avoid “Java-dependency” friction.
- Clean Build: Treat warnings as a learning opportunity; don’t let them accumulate.
- Canary Testing: Use your own CLI as the primary integration test for your library’s API.