handle method

  1. @override
Future<int> handle(
  1. ArtisanContext ctx
)
override

Execute the command. Returns the process exit code.

Implementation

@override
Future<int> handle(ArtisanContext ctx) async {
  // 1. Validate the positional name argument.
  final name = pluginName(ctx);
  if (name.isEmpty) {
    ctx.output.error('Missing required argument: name.');
    return 1;
  }

  // 2. Verify the install record exists. Without it the plugin was either
  //    never installed via the manifest path OR the record was deleted
  //    manually; either way the command cannot derive a reverse plan.
  final root = getProjectRoot();
  final recordPath = p.join(root, '.artisan', 'installed', '$name.json');
  if (!File(recordPath).existsSync()) {
    ctx.output.error(
      'Plugin "$name" is not installed via the manifest path '
      '(no install record at $recordPath). Nothing to uninstall.',
    );
    return 1;
  }

  // 3. Resolve + parse the manifest.
  final manifestPath = await resolveInstallYaml(name);
  if (manifestPath == null) {
    ctx.output.error(
      'No install.yaml found for plugin "$name". The manifest must be '
      'reachable so plugin:uninstall can replay the original install '
      'plan in reverse.',
    );
    return 1;
  }

  final InstallManifest manifest;
  try {
    manifest = ManifestParser.parseFile(manifestPath);
  } on FormatException catch (e) {
    ctx.output.error('install.yaml at $manifestPath: $e');
    return 1;
  } on ManifestValidationException catch (e) {
    ctx.output.error('install.yaml at $manifestPath: ${e.message}');
    return 1;
  }

  // 4. Build the install context once. Both the dry-run preview and the
  //    real uninstall share the same projectRoot + fs seam.
  final installContext = _buildScopedContext(ctx, projectRoot: root);

  // 5. Dry-run short-circuit: render the planned reverse ops and exit
  //    BEFORE the destructive confirm + commit. Op reconstruction mirrors
  //    ManifestInstaller.uninstall's internal flow so the preview matches
  //    what the real run would do.
  if (isDryRun(ctx)) {
    _renderDryRun(ctx, recordPath: recordPath, name: name);
    return 0;
  }

  // 6. Confirm prompt (skipped under --force / --non-interactive). The
  //    interactive path uses [promptConfirmation] which test subclasses
  //    override.
  if (!isForce(ctx) && !isNonInteractive(ctx)) {
    final ok = promptConfirmation(ctx, name);
    if (!ok) {
      ctx.output.info('Uninstall aborted by operator.');
      return 0;
    }
  }

  // 7. Delegate to ManifestInstaller. The reverse transaction deletes the
  //    record on Success (see ManifestInstaller.uninstall).
  final installer = ManifestInstaller(installContext, manifest);
  final result = await installer.uninstall(force: isForce(ctx));

  switch (result) {
    case Success():
      ctx.output.success(result.describe());
      // 8. Mirror the install-time bin/artisan.dart injection: strip the
      //    plugin's import + registerProvider lines so the next
      //    `dart run artisan list` no longer attempts to wire the plugin.
      _stripWrapperLines(
        installContext: installContext,
        pluginName: name,
        providerClassName: manifest.magic.provider,
      );
      // 9. Purge the AOT cli-bundle so the next ./bin/fsa invocation rebuilds
      //    against the updated lib/app/_plugins.g.dart (issue #9 GAP A).
      CliBundleCache.purge(root);
      return 0;
    case DryRun():
      // Unreachable in practice; ManifestInstaller.uninstall has no
      // dryRun parameter and the --dry-run branch returned above.
      ctx.output.info(result.describe());
      return 0;
    case Conflict():
      ctx.output.error(result.describe());
      return 2;
    case Error():
      ctx.output.error(result.describe());
      return 1;
  }
}