runArtisan function
- List<
String> args, { - List<
ArtisanServiceProvider> baseProviders = const <ArtisanServiceProvider>[], - List<
ArtisanServiceProvider> autoProviders()?, - bool delegateToConsumer = true,
- bool collectMcpTools = false,
- WrapperExistsCheck? wrapperExists,
- WrapperNameResolver? wrapperName,
- DelegateStrategy? delegate,
Shared artisan bootstrap.
One entry point consolidates the four behaviors every artisan binary
(substrate fluttersdk_artisan, magic's consumer wrapper, magic_logger's
consumer wrapper, future consumer wrappers) needs:
- Auto-delegation. When invoked from a project that ships its own
consumer wrapper (
bin/dispatcher.dart, or the legacybin/artisan.dart), transparently re-invokedart run :dispatcher <args>so the consumer's full provider list owns dispatch. Bypassed for commands that regenerate the files the wrapper depends on (see_bypassDelegation). - Standalone dispatch. When no wrapper is present (or the call
bypasses delegation, or
delegateToConsumerisfalse), register builtins +baseProviders+ auto-discovered providers and dispatch via ArtisanApplication. - Fail-fast collision. A duplicate command between providers surfaces as exit 2 (matches the substrate bin's prior behavior).
- Generic crash handling. Any unexpected throw surfaces as exit 3 (matches ArtisanApplication's in-handle exception code).
baseProviders are statically known providers the caller wires by hand
(e.g. [MagicArtisanProvider()] from magic's wrapper). autoProviders
is a thunk for codegen-discovered providers (the _plugins.g.dart
autoDiscoveredProviders() function); deferred so a missing or broken
codegen file does not crash callers that pass only baseProviders.
wrapperExists, wrapperName, and delegate are seams for tests;
production callers leave them at their defaults. wrapperName supersedes
wrapperExists when both are injected: the resolver returns the wrapper
filename so the delegate token (:dispatcher for canonical,
:artisan for legacy) matches the file actually present, preventing
dart run :dispatcher ... from failing against a legacy-only consumer.
When collectMcpTools is true, each provider's MCP tool descriptors are
registered via ArtisanRegistry.registerMcpToolsFor immediately after the
provider's commands are registered. Defaults to false so CLI invocations
(e.g. dart run :dispatcher list) pay no MCP overhead. Only the MCP server
entry point (bin/mcp.dart) passes collectMcpTools: true.
Implementation
Future<int> runArtisan(
List<String> args, {
List<ArtisanServiceProvider> baseProviders = const <ArtisanServiceProvider>[],
List<ArtisanServiceProvider> Function()? autoProviders,
bool delegateToConsumer = true,
bool collectMcpTools = false,
WrapperExistsCheck? wrapperExists,
WrapperNameResolver? wrapperName,
DelegateStrategy? delegate,
}) async {
// 1. Decide whether the consumer wrapper owns this invocation. Resolve
// the wrapper FILENAME first (dispatcher vs legacy artisan) so the
// delegate token matches the file actually on disk; an older consumer
// that only ships bin/artisan.dart must keep resolving via :artisan.
if (delegateToConsumer) {
final resolvedName = (wrapperName ?? defaultConsumerWrapperName)();
// Back-compat: existing tests inject `wrapperExists: () => true` against
// tempdirs that have no on-disk wrapper. Honor the boolean override and
// fall back to the canonical 'dispatcher' token in that case.
final hasWrapper =
wrapperExists != null ? wrapperExists() : resolvedName != null;
final firstArg = args.isEmpty ? '' : args.first;
final bypassed = _bypassDelegation.contains(firstArg);
if (hasWrapper && !bypassed) {
final token = resolvedName ?? 'dispatcher';
return await (delegate ?? _defaultDelegate)(<String>[':$token', ...args]);
}
}
// 2. Standalone path: builtins + baseProviders + autoProviders.
try {
final registry = ArtisanRegistry();
registry.registerAll(
_builtinCommands(registry),
providerName: 'fluttersdk_artisan',
);
for (final provider in baseProviders) {
registry.registerProvider(provider);
if (collectMcpTools) registry.registerMcpToolsFor(provider);
}
final auto = autoProviders?.call() ?? const <ArtisanServiceProvider>[];
for (final provider in auto) {
registry.registerProvider(provider);
if (collectMcpTools) registry.registerMcpToolsFor(provider);
}
final app = ArtisanApplication(registry: registry);
return await app.dispatch(args);
} on ArtisanCommandCollisionException catch (e) {
stderr.writeln('Fatal: $e');
return 2;
} catch (e, s) {
stderr.writeln('Unexpected error: $e');
stderr.writeln(s);
return 3;
}
}