runArtisan function

Future<int> runArtisan(
  1. List<String> args, {
  2. List<ArtisanServiceProvider> baseProviders = const <ArtisanServiceProvider>[],
  3. List<ArtisanServiceProvider> autoProviders()?,
  4. bool delegateToConsumer = true,
  5. bool collectMcpTools = false,
  6. WrapperExistsCheck? wrapperExists,
  7. WrapperNameResolver? wrapperName,
  8. 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:

  1. Auto-delegation. When invoked from a project that ships its own consumer wrapper (bin/dispatcher.dart, or the legacy bin/artisan.dart), transparently re-invoke dart 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).
  2. Standalone dispatch. When no wrapper is present (or the call bypasses delegation, or delegateToConsumer is false), register builtins + baseProviders + auto-discovered providers and dispatch via ArtisanApplication.
  3. Fail-fast collision. A duplicate command between providers surfaces as exit 2 (matches the substrate bin's prior behavior).
  4. 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;
  }
}