modular_cli_sdk 0.4.0
modular_cli_sdk: ^0.4.0 copied to clipboard
Command-centric SDK for building modular CLIs with Dart — Command/Input/Output contract, structured errors, output formatting, and automatic TTY detection. Built on cli_router.
Changelog #
All notable changes to this project will be documented in this file.
The format loosely follows Keep a Changelog and the project adheres to Semantic Versioning.
0.4.0 #
A CLI built on this SDK could not say which of its routes change things, and had no way to show what a change would do before doing it. Both are now the SDK's job. See ADR 0002.
Added #
Query<I, O>— a route that reads and answers.validate()andexecute(), which is exactly whatCommandwas until nowCommand<I, O>— a route that changes something, as an ordered list of steps:validate(),steps()anddescribe(Execution). A step states its intention throughpreview()and does its work throughperform(), and the executor compares the two. The preview is therefore checked rather than trusted, which a dry-run flag threaded through the work can never bem.query(...)/m.command(...), and the same pair onModularClifor root routes. Which one a route is registered as decides what the framework does with it, so "this changes something" is a fact about the registration rather than a comment in the file--plan,--applyand--autoapprove, declared on every command and rejected on every query. Neither of the first two is a default: a bare invocation of a command is an error.--autoapproveon its own authorizes nothing and says soApprover— how an approval is taken, injected onModularCli. The default asks on the terminal and refuses rather than hangs when there is no terminal to ask, naming--autoapproveas the way throughPlanSink— where a plan is filed, injected onModularCli. Defaults to nowhere: whether a project keeps plans on disk is that project's decisionPlanOutput/DeclinedOutput— the framework's own answers for "nothing happened yet" and "you said no", so that no command has to model eitherCommandKindon every catalog entry, published byhelp --jsonas"kind". Text help lists queries apart from commands when a CLI has both, and keeps one list when it does notexample/modules/notes/— the example's first writing route, exercised by the suite through both--planand--apply
Changed #
- BREAKING —
Commandno longer hasexecute(). Every existing command is what is now aQuery: changeimplements Command<I, O>toimplements Query<I, O>andm.command(...)tom.query(...). Nothing else about a reading route changes - BREAKING — a command is always enforced. Omitting
paramsno longer leaves it undeclared; it declares that the command takes nothing but the three flags. Queries keep the old behaviour HelpCommandis nowHelpQuery, because help changes nothing — which is also whyhelp --planis rejected without that having to be arranged- A step that acted differently from its own preview is reported on stderr whatever the command chose to say, and does not stop the run: it did do something, and later steps may depend on it. A step that throws stops the run and fails the invocation even when the command reported what it managed
Notes #
- The engine is
preview_executor, a separate package that knows nothing about CLIs.modular_apihas the same problem from the other end of the wire, so the engine belongs to neither.Step,Preview,OutcomeandExecutionare re-exported here, so a command author still imports one package --planwrites a report, not an executable plan.--applynever reads it and re-previews immediately before acting, so there is no saved plan that can go stale, and none of Terraform's staleness machinery is needed
0.3.5 #
Fixed #
-
The documentation no longer teaches a way of running that answers wrongly. A compiled Dart CLI resolves
Platform.resolvedExecutableto itself, so whatever it locates beside its own executable is found where it was installed. Run the same code throughdart runand that path is the Dart binary: the CLI looks inside the Dart SDK, finds nothing, and reports a broken installation that is not broken. Nothing here said so, and## Compile to executablesent the binary tobuild/with nothing next to it — a layout in which the failure is guaranteed. It now teaches the layout an installed CLI has, the binary inbin/withassets/beside itThe Quick start is deliberately unchanged.
dart runis genuinely equivalent for a CLI that locates nothing beside itself, and forbidding it would be stricter than the truth. The limit is taught by demonstration instead -
The roadmap said things that were not true. It announced v0.2.0 and v0.3.0 as planned with the package already at 0.3.4, and promised a
Flagclass whereCliParamwas built, plusCliConfig, profiles andcli context set— none of which exist. It now names what shipped, and what is merely being considered carries no version number, because attaching one to something unbuilt is how it went wrong -
The architecture document never mentioned root commands, which shipped in 0.2.0 and which the README lists as a feature
Added #
example/beside_executable.dart— a CLI that reads an asset from beside its own executable, so the failure above is reproducible in this repository rather than described in it. Built intobin/withassets/alongside, it answers; run from source it names the cause instead of blaming the installationtest/running_from_source_test.dart— pins three facts: the Quick start's CLI answers the same either way, the second example answers differently, and the outputs printed in the README are the ones the commands produce. It compiles executables and is slower than the rest of the suite together- Continuous integration, which this repository had never had —
ubuntu-latestandwindows-latest, mirroringmacss. Nothing here had previously been demonstrated outside Windows
Removed #
AGENTS.md. It restated the README in its own words and drifted from it. The three conventions that lived only there were carried intodocs/architecture.mdfirstdoc/as a home for hand-written documentation. It is git-ignored and left to whatdart docgenerates, which the Dart package layout convention says does not belong under source control. Everything written by hand is indocs/, which is now versioned — it never had a file tracked in it before- The pinned
modular_cli_sdk: ^0.3.0snippet from## Installation, which was two releases stale, and a duplicateddart pub addline beside it
Notes #
- No public API changed.
git diffoverlib/for this release is empty; this is documentation, examples, tests and CI. That is why it is a PATCH
0.3.4 #
Fixed #
-
An incomplete invocation is no longer reported as an unknown command. Typing the beginning of a registered route without reaching its end —
mathwheremath addexists — answeredunknown command 'math'followed by the whole catalog. The name was real; what was missing was the end of it, and the user was sent looking for a typo they had not made. The error path now asks the catalog whether what was typed continues into any route, says so, and lists only those continuationsThe rule is stated over routes rather than modules on purpose.
api graphqlis not a module — it is the first segment of the routeapi graphql compile— so a module-only check would have left it reported as unknown. Prefix-of-a-route covers the module without an action and the half-typed route as the one case they are, and is the simpler rule of the twoA name that begins no registered route keeps exactly the behaviour it had: it is named, and the full catalog follows. The exit code is unchanged for both, and is now pinned by a test rather than inherited
No public API was added. The completions are rendered by narrowing what
HelpRendereris given rather than teaching it a new shape, which keeps it the only place help text is produced
Notes #
- This does not give the surface position help of its own.
api graphql --helpstill renders nothing; what changes is that the error stops calling it unknown. Whether a CLI should have three levels at all is a question for the CLIs built on this SDK, not for the SDK
0.3.3 #
Fixed #
- A command with positionals can be asked for its contract. The router cannot match
show <id>until the id is supplied, soshow --helpfell to the error path — the user had to provide the very argument he was asking about. A command is now named by its route without positional placeholders, so bothshow --helpandhelp showrender its contract.show 1 --helpkeeps working
Added #
CommandContract.name— the route without its positional placeholders, i.e. the tokens a user types to name the commandCommandCatalog.forName— lookup by that name
0.3.2 #
Fixed #
- A command can now declare that it accepts no options, and be enforced.
paramsdefaulted toconst [], so declaring an empty contract was the same value as declaring none: a zero-argument command was indistinguishable from an undeclared one and its arguments went unchecked —init --host fooran, silently doing nothing the flag implied.paramsis now nullable (null= declares nothing, unenforced, as before;[]= declares no options, and any option is rejected)
Changed #
ModularCli.command/ModuleBuilder.commandtakeList<CliParam>? params(wasList<CliParam> params = const []). Source-compatible: omittingparamsbehaves exactly as beforeCommandContract.paramsisList<CliParam>?, withisDeclaredanddeclaredParamsfor the two readings
0.3.1 #
Fixed #
- A registered root route owns the empty invocation.
ModularClirewrote bare<cli>intohelpunconditionally, on the assumption that no route can serve the empty invocation. A CLI that registers one — a dashboard, a status screen, a banner — had that command silently replaced by the help. The rewrite now applies only when nothing claims the empty route; a CLI without a root route is unaffected - The help listing names the root route. Having no token to type, it rendered as a description hanging off a blank column. It is now listed as
(no arguments)— the only way it can be invoked
Added #
- The example registers a root command, so the bare invocation is exercised. Its absence is why no test could see either defect above
0.3.0 #
Added #
- Command contract —
CliParamdeclares a command's parameters (kind, type, short alias, required, default, allowed values) on itsInput, andcommand(...)accepts them viaparams:(#7) - Native help —
help, no arguments,--helpand-hprint the command list to stdout with exit 0. Unknown or invalid usage stays on stderr with exit 64. Ahelpcommand registered by the developer overrides the built-in one - Focused help —
<command> --helprenders that command's contract;<module> --helprenders every command in the module help --json— the full contract catalog as JSON (help.json), the machine twin of the text help, through the existingJsonCliOutput- Enforcement — the declaration governs parsing: aliases resolved, declared defaults applied, values coerced to their declared type, undeclared options and values outside
allowedrejected with exit 7. A rejected invocation is answered with the contract it failed to honour
Changed #
Input.schemaFieldsis now typedList<CliParam>?(wasList<dynamic>?, documented as reserved)- Requires
cli_router: ^0.1.0, which adds theonNotFoundhook the SDK uses to render its own catalog on the error path, and route metadata for positionals
Notes #
- Commands that declare no
paramsbehave exactly as before: not described in help, not enforced
0.2.1 #
0.2.0 #
Added #
ModularCli.command<I, O>()— register root-level commands without a module prefix- Root commands reuse the full
Command<I, O>lifecycle (validate → execute → format) - Root commands honor
--json,--quiet,CommandException, and semantic exit codes - Example
versionroot command inexample/commands/version.dart - 4 new integration tests for root commands
0.1.0 #
Added #
ModularCli— entry point that orchestrates modules, global flags, and TTY detectionModuleBuilder— per-module command registration viacommand()Command<I, O>— abstract unit of work withvalidate()andexecute()lifecycleInput— abstract inbound DTO (deserialize fromCliRequestflags/params)Output— abstract outbound DTO withtoJson()andexitCodeCommandException— structured error withcode,message,details,isRetryableExitCode— semantic exit code constants (0, 1, 2, 4, 5, 6, 7, 64)CliOutput/JsonCliOutput/TextCliOutput— output formatting abstraction--jsonglobal flag — machine-readable JSON output--quiet/-qglobal flag — suppress informational messages- Working example with two modules (greetings + math)
- Full test suite (unit + integration)