ag_flow_cli 0.1.0
ag_flow_cli: ^0.1.0 copied to clipboard
The `ag` CLI for ag_flow — one command generates a fully-wired feature module (page, controller, repo, service, binding, component) and wires its route, argument, and navigation method automatically.
Changelog #
Unreleased #
-
ag initnow scaffolds the architecture standard, not just code. The goal is that a developer or a coding agent picking up a project already knows the rules, instead of being told them again every session.AGENTS.mdat the project root — the layer contract, the folder structure, the generator commands to run instead of writing files, and an explicit "you must not" list.AGENTS.mdis the cross-tool convention coding agents read automatically;CLAUDE.mdis a pointer to it rather than a second copy to drift..github/workflows/ag.yaml— runsag analyze, formatting, static analysis and tests on every push. Instructions are advisory; this is the gate, and it is free on public repositories..githooks/pre-commit(opt-in viagit config core.hooksPath .githooks) — the same check locally, so a violation is caught before CI. Written with the executable bit, which git silently requires.- All four are
skipExistinglike every other generated file, so re-runningag initnever overwrites an edited standard. - The hook resolves the CLI through the project's own
ag_flow_clidev_dependency before anything onPATH. A globalagmay be a different version — or, since the name is short, a different tool entirely; found the hard way when a globally-activated unrelatedagshadowed it and its missinganalyzecommand was reported as an architecture violation.
-
ag --version/ag -v. Works with no sub-command, as every CLI should — and as any script probing for the tool will try first. -
Typed models:
ag g m product --from-json=sample.json. Paste a real API response and the generated module compiles against your data — the model class is generated and its type threaded through the Service, Repo, Controller, Page and item component. Previously a fresh module handed back 17dynamicplaceholders across 5 files to replace by hand, which is the opposite of what a generator is for.--model=Productalone names the type without inferring fields.- Inference handles nested objects (as nested classes), lists of
objects and of scalars,
snake_case/camelCase/kebab-casekeys (keeping the original JSON key for encoding), Dart keyword collisions, and reads doubles throughnumso a whole-number payload doesn't throw. Anullsample value becomesObject?rather than a guess. - A list response and a single-key envelope (
{"data": [...]}) are both understood, so a real response body works unedited. Only known envelope keys are looked through —{"rating": {...}}is a one-field model, not an envelope. - A detail module reuses its root's model (
product/detailsgetsProduct), and itsidOfis generated with the path-parameter conversion already written (int.parse(argument.id)). - Generated models are plain data classes: no
build_runner, no annotations, no part file. The file is skipped if it already exists, so edits survive regeneration. - Entirely additive: without
--model/--from-jsonthe output is unchanged.
-
The CLI's own description no longer says "Flutter/GetX" — the GetX dependency was removed some releases ago.
-
Generated Services are now pure declaration — no stub bodies at all. A generated collection Service is
with AgCrudService<..>, AgPagedService<..>plus five declared values (two endpoints, apageStrategy, anenvelope,fromJson/toJson); a detail Service is the same minus paging, plus a singleidOfhook naming which field of the navigation argument identifies the resource. There is no request, decoding, orhasMorecode left to write or delete.- Consequently
--methods=now shapes the Repo and Controller passthroughs, not the Service. Its original purpose was avoiding stub bodies a developer had to delete; the mixins provide those methods for free, as zero lines of code, so there is nothing left to opt out of at the Service layer. - The detail Repo calls
updateByArgument/deleteByArgument— distinct names becauseAgCrudService.update(ID, T)already occupiesupdate.
- Consequently
-
Generated code no longer references GetX. Following
ag_flow's removal of thegetdependency, every template and aggregator updater now emits AG's own primitives:AgBinding+AgLocator.findin bindings,AgRoute/AgTransitioninapp_pages.dart,AgNavigator.toNamedinroute_management.dart, andAgAppin themain.dartwiringag initprints.ag analyze's hard-coded-route check matchesAgNavigator.toNamed('/literal'), and its route-wiring check matchesAgRouteentries. Golden fixtures regenerated from real tool output, as always. -
The Controller's generated update stub is named
updateagain, notupdateItem. That workaround existed solely becauseGetxControllerdeclared its ownupdate([List<Object>? ids, bool condition])and a same-named override was a realinvalid_overrideerror;AgBaseControllerextendsChangeNotifiernow, which declares noupdate. Removing it also resolves a naming hazard it had introduced —updateItemsat one character fromAgPaginationMixin.updateItems— and restores symmetry, since Service and Repo always usedupdate. -
Reverted the success-content split from the "pure wiring" page change below:
_list.dart(collection) and_view.dart(detail) are gone. Neither was a genuinely reusable widget — each was just the one page's own body, wrapped in an extra class for no reason, unlike the app bar/ loading/error/empty slots (real, independently-overridable AG framework concepts) or the per-row_item.dart(a real reusable component).buildSuccessis inline in the page again: a collection page directly constructsAgListBuilderreferencing_item.dart; a detail page keeps its original inline placeholder. -
Fixed:
ProjectAnalyzer.dependenciesResolvednever detected resolution for a native-pub-workspace member project — it only checked<project root>/.dart_tool/package_config.json, but a workspace member's resolved config lives only at the workspace root, never duplicated per member.ag analyzerun from inside such a project always silently skipped the resolved-model checks, even fully resolved. Fixed withpackage:package_config'sfindPackageConfig(walks up parent directories, mirroring how Dart's own tooling resolves package configs for workspace members), added as a direct dependency. Found by generatingpackages/ag_flow/example— a real workspace member — and runningag analyzefrom inside it directly. -
Fixed two
very_good_analysisfindings against generated code, found the same way: a required named constructor parameter declared after an optional one (super.key) in every chrome component with a required field — reordered required-first; andGet.toNamed(...)'s untyped generic call in every generated navigation method — now emitsGet.toNamed<dynamic>(...). Three more lints (flutter_style_todos,always_use_package_imports,discarded_futures) don't fit generated/ aggregator-file conventions at all and are disabled, with a rationale comment, inpackages/ag_flow/example's ownanalysis_options.yamlrather than papered over in the shared root config every package uses. -
Every generated module now lives under a shared
lib/modules/parent —lib/modules/product/...,lib/modules/auth/...— instead of directly underlib/, sitting alongside (never inside)lib/core. Filesystem organization only: route paths, class names, and every other naming derivation are unchanged.ag analyze's structural and resolved-model checks look underlib/modules/accordingly. Found and fixed along the way: Mason's default{{var}}interpolation HTML-escapes its value, so the newmodule_import_pathvar (the first brick var whose value contains a/) rendered asmodules/productin every brick-templated import until switched to unescaped triple-mustache ({{{module_import_path}}}). -
ag g m's generated page is now pure wiring — every overridable slot is its own component file, generated and linked in automatically.appBar,loadingBuilder,errorBuilder, andemptyBuildereach get a dedicated file undercomponents/<namespace>/(_appbar.dart,_loading.dart,_error.dart,_empty.dart), and the success content does too: a collection module's_list.dart(wrapsAgListBuilder, referencing the existing_item.dartper-row widget — previously generated but never actually wired into the page) and a detail module's_view.dart(previously generated but likewise dangling). All are plain starting-point widgets — customize freely, or delete one and its one-line reference in the page to fall back toAgBasePage's own default for that slot.- Found and fixed along the way, via a real
dart analyzeagainst a freshly generated,pub get-resolved project (this repo's own verification convention, but not previously re-run after the add/update/delete stub feature below shipped): the generated Controller'supdate()stub silently collided withGetxController .update()— an override with an incompatible signature, a genuineinvalid_overridecompile error in every project that generated one. Renamed toupdateItemon the Controller only (Service/Repo keepupdate()— they don't extendGetxController, so there's no clash).
- Found and fixed along the way, via a real
-
ag g mnow generatesadd/update/deletestubs by default across Service/Repo/Controller, alongside the always-present read method (getPage/getByArgument) — matching the read method's own shape exactly (a plainUnimplementedErrorplaceholder with aTODOpointing atcore/endpoints.dart). Not a mixin, not framework-owned: a generator's whole point is to hand over more working boilerplate than starting from scratch, and every stub is deleted exactly as freely as any other generated code if a module doesn't need it.addis only generated for collection modules — a detail module has no "create a new one" concept. New--methods=flag controls which get generated (--methods=add,delete, or--methods=nonefor read-only); omitting it generates all applicable ones. Golden fixtures regenerated from real tool output, not hand-transcribed, matching this repo's own existing convention for avoiding formatter-output guessing errors. -
Fixed:
--plural=was documented and fully implemented at theModuleGenerator/ModuleSpeclayer, but never wired intoag g mitself —ModuleCommandnever registered the option, so it was unreachable from the actual CLI. Wired viaargParser.addOption ('plural', ...); the supplied override is run through the samepascalCaseconversion the naming machinery already uses for every other segment, since an early version of this fix passed the raw CLI argument straight through and producedcompaniesControllerinstead ofCompaniesController. Covered by a newtest/src/commands/module_command_test.dart, exercising the realargs-parsing/CommandRunnerpath rather than only the generator API directly (which is exactly where the wiring gap — and the casing bug — were both invisible before). -
ag analyzev2 checks — dependency-direction violations and unused detail arguments, both previously deferred pending a resolved element model. Built onpackage:analyzer'sAnalysisContextCollection(resolved units, not justparseString) instead of the syntax-only approach every other check uses:- Dependency-direction: flags a Page directly referencing a Repo or
Service, a Controller directly referencing a Service/
ApiProvider/Dio, or a Repo directly referencingApiProvider/Dio— matched against the referenced type's full supertype chain (viaInterfaceType.allSupertypes), so a concreteProductsRepois caught the same way a rawAgBaseReporeference would be (requirments/ag_framework.md §11/§70). - Unused detail argument: flags a detail controller (one extending
AgDetailController) whose inheritedargumentsaccessor is never referenced anywhere in the class — resolved-model-based specifically so a reference is confirmed to resolve to the real inherited getter, not just a same-named local that would false-positive a syntax-only search. - Both checks are skipped (not a failure) if the project's dependencies
haven't been resolved yet (no
.dart_tool/package_config.json) —ag analyzeprints a notice telling the user to runpub getfirst, while every other (syntax-only) check still runs normally. - Verified end-to-end via
dart runand a realdart pub global activate-installedagagainst a genuineflutter pub add-wired project. A standalonedart compile exebuild does not work for these two checks — the analyzer can't auto-detect the Dart SDK from a self-contained native binary — documented as a known limitation since that isn't this package's supported distribution method anyway. ProjectAnalyzer.analyze()is nowasync(Future<List<AnalyzeIssue>>) to support this; every existing syntax-only check is unaffected and every existing test still passes unchanged.
- Dependency-direction: flags a Page directly referencing a Repo or
Service, a Controller directly referencing a Service/
0.1.0 #
Initial release.
Commands
ag init— bootstrapslib/core/skeleton (arguments.dart,endpoints.dart,routes/{app_routes,app_pages,route_management}.dart,bindings/initial_binding.dart). Idempotent — safe to re-run; existing files are never overwritten.ag g m <path>(ag generate module) — generates the complete module for a collection (root) or detail (child) path:- Five architectural-layer files: page, controller, repo, service, binding.
- Per-row item component and four override-slot components (appbar, loading, error, empty).
- Idempotent wiring into
app_routes.dart,app_pages.dart,route_management.dart, and (for detail modules)arguments.dart. --model=Name/--from-json=sample.json— generates a typed model class and threads it through all five layers; handles nested objects, lists,snake_case/camelCase/kebab-casekeys, Dart keyword collisions, andnullsample values.--plural=override— overrides the default pluralisation for the collection-layer class and file names.--dry-run— prints what would be written without touching the filesystem.
ag analyze— validates a project against AG's structural rules:- Missing architectural-layer files.
- Routes missing navigation wiring, argument class, or nav method.
- Duplicate route paths.
- Hard-coded
AgNavigator.toNamed('/literal')calls outsideroute_management.dart. - (Resolved-model) Dependency-direction violations: Page→Repo, Page→Service, Controller→Service, Repo→ApiProvider.
- (Resolved-model) Detail controller whose inherited
argumentsgetter is never used.
ag --version— prints the CLI version.
Generator quality
- All output goes through the same
FileOp/Executorboundary — dry-run mode is not an afterthought. - Aggregator file updates are offset-splice AST edits (syntax-only
package:analyzer) — an existing hand-customised navigation method survives regeneration byte-for-byte. RouteConflictException— a route constant pointing at a different path than this module would derive is reported as a distinct error, never silently overwritten.- Parent-existence validation for child/detail modules — partial writes never happen on failure.
- Golden fixtures regenerated from real tool output, not hand-transcribed.
- Fully cumulative naming derivation (
ModulePath/ModuleSpec), with root-module layer pluralization handled separately from (never- pluralized) component naming. - Parent-existence validation for child/detail modules, with the exact spec-mandated error message and no partial writes on failure.
--dry-runsupport; idempotent re-generation (never overwrites an existing file).- Idempotent updates to the shared aggregator files (
app_routes.dart,app_pages.dart,route_management.dart, and — for detail modules —arguments.dart) via offset-splice AST editing (package:analyzer, syntax-only parse): a new entry is inserted only if missing; an existing entry — including a hand-customized navigation method — is left byte-for-byte untouched. Requires the project to already have theag initskeleton in place (that command itself doesn't exist yet). RouteConflictException: a route constant that already exists pointing at a different path than this module would derive is reported as a distinct conflict, never silently overwritten.ag init: bootstraps a bare project'slib/core/skeleton (arguments .dart,endpoints.dart,routes/{app_routes,app_pages,route_management} .dart,bindings/initial_binding.dart) soag g mhas somewhere to wire generated modules into. Idempotent via the sameFileOp/Executorboundary asag g m— an existing file is skipped, never overwritten. Verified end-to-end against a realflutter created app:ag init→ag g m product→ag g m product/details→flutter analyze --fatal-infosreports zero issues.ag analyze: a v1, mechanical/structural-only validator. Re-parsesapp_routes.dart's route table and re-derives each route's expectedModuleSpecfrom its path (not its constant name, which is what makes this immune to whatever pluralizer produced the name), then checks: missing architectural-layer files, missingGetPage/nav-method/argument- class wiring, two route constants pointing at the identical path, nested architectural/component folders, and hard-codedGet.toNamed('/literal')calls outsideroute_management.dart. Exits0clean, non-zero with every issue printed otherwise. Dependency-direction violations and detail-route-without-argument-usage checks are deliberately deferred to v2+ (need a resolved element model, not just syntax) — a regression test confirms a Page hand-edited to call a Service directly produces zero issues today, documenting that boundary explicitly rather than leaving it implicit.