fluttersdk_artisan 0.0.5
fluttersdk_artisan: ^0.0.5 copied to clipboard
Composable Dart CLI framework and stdio MCP server for Flutter. Scaffolding, code generation, transactional plugin installs, hot reload, REPL, and AI agent tooling.
Changelog #
All notable changes to this project will be documented in this file.
This project follows Semantic Versioning 2.0.0. Entries follow the Keep a Changelog shape.
[Unreleased] #
0.0.5 - 2026-05-23 #
Fixed #
./bin/fsaAOT bundle staleness missedlib/app/_plugins.g.dartmtime (issue #9 GAP A): afterplugin:installregeneratedlib/app/_plugins.g.dart, subsequent./bin/fsainvocations kept running the stale cached bundle, so newly registered plugin commands silently did not surface. Fixed by two complementary changes: (a) appended condition-5 (_plugins.g.dart -nt STAMP_FILE) tobin_fsa.sh.stub'sneeds_build()shell function so the shim self-heals on any plugin operation regardless of who mutated the file, and (b) addedCliBundleCache.purge(projectRoot)to the legacyplugin:installsuccess path,plugin:uninstallsuccess path, andplugins:refreshsuccess path so the cache invalidates as a direct side effect of artisan-managed plugin lifecycle events. Manifest-flowplugin:installdelegates toplugins:refreshtransitively, so a single purge call covers both. Migration: re-runmake:fast-cli --forceto pick up the new shim. No CI or publish changes.- MCP
dusk_evaluatereturned a sentinel string instead of evaluating (issue #9 GAP F): the host-sideext.dusk.evaluatehandler influttersdk_duskreturns a no-op sentinel by design; the actual evaluation must run throughvm.evaluate.lib/src/mcp/mcp_server.dart_dispatchnow special-casesdusk_evaluateby tool name and routes throughVmServiceClient.evaluate(isolateId, expression)directly, with 3-branch error handling per the VM Service spec (InstanceRefhappy path;ErrorRefruntime exception surfaced asisError: true;Sentinelstale-isolate with actionable hint;RPCErrorcode 113 compile error with details extracted). Coordinated bump pairing:fluttersdk_dusk0.0.2 plans to bump the artisan constraint to^0.0.5. - MCP
serverInfo.versionno longer drifts (issue #9 NIT 7): the hardcodedversion: '0.0.1'inlib/src/mcp/mcp_server.dartlagged the pubspec across four releases. This release manually syncs the literal to'0.0.5'as part of the release-cut commit. A future patch may switch to a build-time_kArtisanVersionconstant to eliminate manual drift recurrence (deferred per scope). TheserverInfo.nameliteralfluttersdk_artisan_mcpstays as-is; the MCP spec treatsserverInfo.nameas a display hint, and Claude Code derives tool prefixes from the.mcp.jsonkey, not from the server-advertised name. Seedoc/mcp/setup.md#server-identityfor the rationale.
0.0.4 - 2026-05-21 #
Fixed #
- MCP server returns empty tools/list (issue #7, Bug A):
dispatcher.dart.stubnow forwardscollectMcpTools: args.isNotEmpty && args.first == 'mcp:serve'torunArtisan, so plugin providers'mcpTools()collect into the registry when consumers invoke./bin/fsa mcp:serve. Migration: substrate-installed consumers re-rundart run fluttersdk_artisan install --forceto regeneratebin/dispatcher.dart. Magic-installed consumers need a paired magic-side stub update (tracked separately) beforemagic:artisan install --forcepropagates the fix. mcp:installwrites the canonical post-install entry shape (issue #7, Bug B):.mcp.jsonentry branches between./bin/fsa mcp:serve(POSIX withbin/fsapresent) anddart run :dispatcher mcp:serve(Windows or no-fsa fallback); the previous hardcodeddart run fluttersdk_artisan:mcpshape routed through the substrate standalone, which never loads consumer plugin providers. Migration: consumers must re-run./bin/fsa mcp:install(ordart run fluttersdk_artisan mcp:install).- Auto-delegation now resolves the canonical consumer wrapper:
_defaultDelegateatlib/src/console/run_artisan.dartpreviously emitteddart run :artisan, which resolves only tobin/artisan.dart. Post-0.0.2 the canonical wrapper isbin/dispatcher.dart. Fixed by prepending:dispatcherupstream of the delegate call ((delegate ?? _defaultDelegate)([':dispatcher', ...args]));_defaultDelegate's body simplifies to['run', ...args]. Latent since 0.0.2; not caught by tests because the existing delegation tests mockeddelegate:without asserting on the prefixed args. doctoradvisory extended for pre-Bug-B.mcp.json:doctornow advisory-warns when.mcp.jsonstill containsfluttersdk_artisan:mcpargs, pointing the user at./bin/fsa mcp:installto upgrade. Does not affect exit code../bin/fsarebuilt AOT bundle on every invocation: the staleness check comparedpubspec.yamlmtime againstpubspec.lock.dart pub addupdatespubspec.yamlafter pub get writes the lock, leavingpubspec.yamlmtime newer thanpubspec.lockfor every freshly installed consumer; that tripped the check on every call. Compare against the build stamp file instead (written at the end of every successful compile), sopubspec.yamlnewer than the stamp means the user actually edited it. Cached invocations now hit the ~50ms target. Discovered during A-Z e2e testing.make:commandcrashed with "Stub file not found: artisan_command.stub": the stub asset never shipped in the publish archive even thoughMakeCommandCommand.getStub()declared it as the canonical scaffold name. Added the missingassets/stubs/artisan_command.stubwith the canonicalfinal class ... extends ArtisanCommandshape honoring{{ className }}/{{ namespace }}/{{ commandName }}placeholders. Discovered during A-Z e2e testing.
0.0.3 - 2026-05-21 #
Changed #
xmlconstraint downgraded^7.0.0->^6.5.0(pubspec.yaml): pub.dev resolution now intersects withimage ^4.0.0(used byfluttersdk_dusk'sext_screenshot.dartviaxml ^6.0.1). The 0.0.2 cut pinnedxml ^7.0.0, which madefluttersdk_duskunresolvable as a hosted dep alongsidefluttersdk_artisan 0.0.2because noimage 5.xexists to satisfy the upper bound. Reverted the 8XmlName.parts('localname')migration sites inlib/src/helpers/plist_writer.dartback toXmlName('localname')so the file compiles cleanly against xml 6.x (where.partsdid not yet exist). xml 7 migration is deferred untilimageships a release on the xml 7 line.
0.0.2 - 2026-05-20 #
Breaking #
consumer:scaffoldrenamed toinstall: the commandconsumer:scaffoldno longer exists. Consumers must usedart run fluttersdk_artisan installgoing forward.bin/artisan.dartrenamed tobin/dispatcher.dart: the scaffold output path has changed. Migration: re-rundart run fluttersdk_artisan install --forceto scaffold the new file layout, then update any scripts or CI steps that referencebin/artisan.dart.- Old stub removed:
consumer_artisan_bin.dart.stubis gone; the replacement stub isdispatcher.dart.stub. InstallCommand->InstallArtisanCommand: the public class on thepackage:fluttersdk_artisan/artisan.dartbarrel now carries theArtisanprefix so plugins exporting their ownInstallCommand(notifications, deeplink, etc.) no longer collide with the substrate at import time.
Added #
installauto-chainsmake:fast-cli: after writing the consumer entry and barrels,installautomatically runsmake:fast-clisobin/fsa(the AOT-compiled fast startup wrapper) is ready without a separate manual step.- New stub
dispatcher.dart.stub: replaces the formerconsumer_artisan_bin.dart.stub; rendered tobin/dispatcher.dartduringinstall. artisan start --cdp-port=Nopt-in flag (lib/src/commands/start_command.dart): when set, pre-launches Chrome with--remote-debugging-port=N --remote-allow-origins=* --user-data-dir=/tmp/dusk-chrome-N, runsflutter run -d web-server --web-port=N --web-experimental-hot-reload --host-vmservice-port=N(silent remap from--device=chrome), waits for the "is being served at" log line, navigates Chrome to the served URL via inline CDP, then scrapesvmServiceUrifrom the DWDS log once the debugger client connected. WriteschromePid+cdpPort+tmpProfileDirto~/.artisan/state.json. Default flow (no--cdp-port) unchanged. Gates the branch onflutter --version --machine>= 3.30.0 with an actionable upgrade error.artisan stopChrome cleanup: whenstate['chromePid'] != null, sends SIGTERM, waits the grace period, escalates to SIGKILL if the process is still alive, deletestmpProfileDir. Inlines the SIGTERM-grace-SIGKILL pattern fromfluttersdk_dusk/lib/src/utils/chrome_reaper.dart:216-264to avoid inverting the plugin dependency direction (see Deferred Ideas: V1.x consolidation).artisan doctorFlutter SDK gate: new checkflutter sdk >= 3.30.0 (for --cdp-port)registered in the existing_Checklist. Advisory_cdpUpgradeWarningwriteln (mirrors_checkStaleMcpJsonpattern) surfaces an upgrade message when the SDK is too old. Required forflutter/flutter#170612(DWDS WebSocket hot reload on-d web-server).StateFileschema: newcdpPortfield (int | null, --cdp-port value passed to start; null when CDP not enabled). Roundtrip test added.- GitHub Release auto-creation in
publish.yml: newgithub-releasejob (depends on the OIDCpublishjob) extracts the## [<version>] - <date>block fromCHANGELOG.mdviaawkand creates a matching GitHub Release usingsoftprops/action-gh-release@v2. Falls back to a stub body linking toCHANGELOG.mdwhen the section is missing. make:fast-clibuiltin command +bin/fsawrapper (lib/src/commands/make_fast_cli_command.dart,assets/stubs/bin_fsa.sh.stub): scaffold a POSIX shell wrapper that compilesbin/dispatcher.dartinto an AOT binary viadart build cli, cached at.artisan/cli-bundle/bundle/bin/dispatcher. Wrapper auto-detects staleness (pubspec.lock SHA256 + Dart SDK version + pubspec.yaml mtime greater than pubspec.lock) and re-compiles transparently. Result: ~50ms startup for./bin/fsa <cmd>vs ~3s fordart run fluttersdk_artisan <cmd>(no "Running build hooks..." overhead). Idempotent on re-run;--forceoverwrites the wrapper. POSIX-only V1 (macOS + Linux); Windows .cmd variant deferred. The existingdart run fluttersdk_artisanpath is unchanged and remains the canonical CLI entry.
Changed #
plugin:installpreflight scope: the wrapper-presence check (bin/artisan.dartmust exist) moved out of the shared preflight into the legacy-injection branch only. Canonical-scaffold projects (lib/app/_plugins.g.dartpresent) now route through.artisan/plugins.jsonregistration without tripping the legacy gate, even when the consumer never wrote abin/artisan.dartfile.artisan start --vm-service-portnow plumbs through toflutter runas--host-vmservice-port=Nand is recorded instate.jsonso downstream tools see the actual bound port. The option was declared but never read in 0.0.1.publish.ymltriggers narrowed topush.tagsandworkflow_dispatch. Removed therelease.types: [published]trigger to avoid release/publish recursion (the workflow creates the release itself now). Tag-first flow:git tag X.Y.Z && git push origin X.Y.Z-> validate -> pub.dev publish via OIDC -> GitHub Release with CHANGELOG-driven notes.
Fixed #
start --cdp-portordering deadlock: 0.0.1 scraped the VM Service URI before navigating Chrome, which deadlocked under DWDS (the URI emits only after a debugger client connects). Restructured the branch to wait for the "is being served at" log line, navigate Chrome to the served URL, then scrape. Three end-to-end Chrome / CDP automation issues fixed alongside:--no-first-run+--no-default-browser-checkon the launch argv,Page.navigatenow targets the page-level WebSocket from/jsoninstead of the browser-level/json/version, automation-noise suppression flags added.start --cdp-port=<non-int>now returns exit 1 with an actionable error instead of silently falling through to the non-CDP path.stopno longer emitsChrome SIGTERM sent...unconditionally: the boolean fromProcess.killPidis checked and anot deliveredwarning surfaces when the signal could not land (process already gone, permission denied).doctorSDK gate now tolerates beta channel strings like3.30.0-1.0.preand missing trailing segments (3.30), matchingStartCommand.compareSemverexactly so the doctor cannot flag a version the start command would accept.- VM Service retries once on the transient DWDS
WipError: Promise was collectedand on the stale-isolate sentinel fromcallServiceExtension, so a single device-target switch or DWDS hiccup does not surface to consumers. installconsumer-wrapper detection accepts bothbin/dispatcher.dart(canonical post-rename) andbin/artisan.dart(legacy) as valid wrappers for auto-delegation.InstallArtisanCommand.scaffoldIntoauto-triggersPluginsRefreshCommandin-process when<root>/.artisan/plugins.jsonexists so the codegen barrel does not get overwritten with an empty list.bin/fsaPID-aware lock recovery: when a prior./bin/fsainvocation crashed mid-build the wrapper used to deadlock on.artisan/.fsa.lockfor every subsequent run. The stub now reads the holder PID, verifies the process is still alive, and reclaims the lock when it is not.
Known limitations #
- MCP schema drift for
artisan_start: the hand-authored_commandInputSchema('start')atlib/src/mcp/mcp_server.dartdoes NOT advertise the new--cdp-portflag. The substrate dispatch still routes CLI args through correctly, but agents drivingartisan_startvia MCP cannot discover the flag from the schema. V1.x backlog: auto-derive the schema fromArtisanCommand.signature/configure(ArgParser)so it cannot drift.
0.0.1 - 2026-05-19 #
Initial public release of fluttersdk_artisan. Pure Dart 3.4+ CLI framework and stdio MCP server for Flutter and Dart projects. Pana score 160 / 160 on first publish.
Commands #
21 builtin commands across 6 groups:
- Lifecycle:
start [--device],stop,restart,status,logs [--follow],reload,hot-restart. - Scaffolding:
consumer:scaffold(canonical wrapper for plain Flutter),make:plugin <name>(plugin package skeleton with workspace enrollment + magic-mode upgrade detection),make:command <Name>(context-aware command scaffold for plugin or consumer). - Plugin management:
plugin:install <name>(manifest-driven, scaffold-aware, or legacy injection),plugin:uninstall <name>,plugins:refresh,commands:refresh. - MCP:
mcp:serve(stdio JSON-RPC server with three-layer filter),mcp:install(writes.mcp.jsonentry, idempotent),mcp:uninstall. - Introspection:
doctor(preflight checks),list(all registered commands grouped by:namespace),help <cmd>. - REPL:
tinker [--eval=<expr>](VM Service evaluate against the running Flutter app; interactive mode falls back when--evalis absent).
Stdio MCP server #
- Built on
dart_mcp ^0.5.1. Entry point:dart run fluttersdk_artisan:mcp. - 10 substrate tools (always-on) surface artisan's own CLI as MCP tools so an LLM agent can bootstrap a Flutter app without leaving the chat: lifecycle quartet (
artisan_start/artisan_stop/artisan_restart/artisan_reload/artisan_hot_restart) plusartisan_status,artisan_logs,artisan_doctor,artisan_list,artisan_tinker. - Plugin tools register via
ArtisanServiceProvider.mcpTools(). The MCP server collects them at startup;ArtisanMcpToolCollisionExceptionattributes name clashes to specific providers. - Three-layer Cargo-style filter:
.artisan/mcp.json(file) +ARTISAN_MCP_TOOLS_*/ARTISAN_MCP_PACKAGES_*(env) +--include-tool/--exclude-tool/--include-package/--exclude-packageCLI flags. Allow uses first-non-null; deny is the union; deny wins everywhere. - Soft-fail at initialize when no Flutter app is running; lazy-reconnects to VM Service on the next tool call. Tool calls without a running app return an actionable
CallToolResult(isError: true)so the model can self-correct.
Plugin protocol #
- Declarative
install.yamlmanifest:publish,magic.provider,magic.configFactory,magic.routes,native.android(permissions / metaData / gradle plugins / dependencies),native.ios/native.macos(plistEntries / podEntries),native.web(headInjections / metaTags),env,prompts,placeholders,bootstrap_command. - Procedural escape hatch: subclass
ArtisanInstallCommandand drivePluginInstallerfor plugins that need runtime branching the schema cannot express.
PluginInstaller DSL #
Fluent builder for install operations across file ops (publishConfig, writeFile, mergeJson), source-injection ops (injectImport, injectBefore, injectAfter, injectProvider, injectConfigFactory, injectRoute), native ops (injectAndroidPermission, injectAndroidMetaData, injectAndroidGradlePlugin, injectAndroidGradleDependency, injectIosPlistEntry, injectIosPodEntry, injectMacosPlistEntry, injectMacosPodEntry, injectIntoWebHead, addWebMetaTag), and env ops (injectEnvVar). Operations enqueue against a sealed InstallOperation hierarchy with 26 final variants.
Idempotency, atomicity, reversibility #
ConflictDetectorflagsunmanaged-filewhen a target exists outside any recorded install (--forceoverride + scaffold-fingerprint heuristic auto-allows the default Flutter counter-app overwrite).InstallTransactionwrites via.tmp+ atomic rename; concurrent readers never observe partial state.ConfigEditor.insertCodeAfterPattern+insertCodeBeforePatternearly-return when the target already contains the code (idempotent re-install).PluginInstaller.injectProvider+injectConfigFactoryappend to the END of the list using lookahead-anchored regex(?=\s*\n\s*\])so new entries appear where readers expect them (6-space indent matches the scaffold style).InstallTransactionrecords every applied op to.artisan/installed/<plugin>.json(op type, target path, content hash).plugin:uninstallreversesWriteFile(delete + stub-hash tamper check);InjectImportandInjectAfterPatternlog[skipped](anchor-bracketed inject markers pending V1.1).
Signature DSL #
Command surface declared inline: String get signature => 'cmd:name {arg} {--flag=default}'. configure(ArgParser) remains available as an explicit fallback. The MCP server's per-command inputSchema is verified against the underlying command's argument declarations so the wire contract cannot drift from the CLI surface.
Codegen barrels #
lib/app/commands/_index.g.dart(consumer commands), regenerated bymake:commandandcommands:refresh.lib/app/_plugins.g.dart(plugin providers), regenerated byplugin:install <name>andplugins:refreshfrom the.artisan/plugins.jsonregistry.
Both write through .tmp + atomic rename; never hand-edit.
VM Service hooks #
tinkerevaluates Dart expressions against the connected isolate via the VM Service evaluate RPC. Magic facade autocomplete + Eloquent model casting come from the optionalmagic_tinkerintegration when registered.reload/hot-restartwriter/Rto theflutter runprocess's stdin via a POSIX FIFO bridge so detached processes still accept interactive commands.
Testable primitives #
VirtualFsinterface +InMemoryFsimplementation. Every installer pathway is unit-testable without touching the host filesystem.InstallContext.test(fs, prompt, stubs, clock, projectRoot)fixture builder.ArtisanContext.bare(MapInput, BufferedOutput)for command-level tests.BufferedOutputcapturesinfo/success/warning/errorlines for assertion.
Programmatic API #
runArtisan(args, baseProviders:, delegateToConsumer:, collectMcpTools:): universal entry point.- Single barrel:
package:fluttersdk_artisan/artisan.dartexposes the full public surface (Application,Command,Input/Output,ServiceProvider,Context,VmServiceClient,StateFile, stub system, helpers, installer, registry).
CI + automated publishing #
.github/workflows/ci.yml: format + analyze + tests + 80 % line-coverage floor (viacoverage:format_coverage+ awk gate) + dry-run archive on every push to master and every pull request..github/workflows/publish.yml: SemVer tag push triggers validate -> pub.dev publish via the officialdart-lang/setup-dart/.github/workflows/publish.yml@v1reusable workflow with OIDC authentication (no long-lived secret stored). Requires "Automated publishing from GitHub Actions" enabled on the pub.dev package admin page with the repository pinned tofluttersdk/artisan..github/dependabot.yml: weekly pub bumps (root +example/) plus weekly GitHub Actions version bumps..github/ISSUE_TEMPLATE/: structuredbug_report.yml,feature_request.yml,documentation.yml, plus aconfig.ymlthat disables blank issues. Bug + feature templates use a 14-option Subsystem dropdown matching thelib/src/layout.
Documentation #
README.mdtwo-path Quick Start (plain Flutter viaconsumer:scaffold; Magic-managed viamagic:install).- 17-file
doc/tree underhttps://fluttersdk.com/artisan/X/Y:getting-started/,commands/,mcp/,plugins/,reference/. skills/fluttersdk-artisan/: LLM-agent skill (SKILL.md+ 5 references:commands.md,install-yaml-schema.md,installer-dsl.md,mcp-server.md,plugin-authoring.md).llms.txtat repo root per llmstxt.org spec.
Compatibility #
- Dart SDK
>=3.4.0 <4.0.0. Pure Dart core; Flutter optional (only required by plugins that consume Flutter SDK APIs). - Platforms: Android, iOS, macOS, Linux, Windows. Web unsupported (relies on
dart:io). - V1 lifecycle commands (
start,stop,reload,hot-restart) use POSIX FIFO stdin pipes viamkfifo. macOS and Linux only; Windows unsupported for the lifecycle quartet (other commands work).