handle method
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;
}
}