diff --git a/AGENTS.md b/AGENTS.md index cf85268b..c3edb7e1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ This file provides guidance to Claude Code and other AI assistants working on th SuperDeck is a Flutter presentation framework that renders slides written in Markdown. Users write slides in a `slides.md` file using Markdown syntax with custom block annotations, and SuperDeck renders them as a Flutter application. - **Live demo**: https://superdeck-dev.web.app -- **Repository**: https://github.com/btwld/superdeck +- **Repository**: https://github.com/conceptadev/superdeck ## Project Structure @@ -17,20 +17,24 @@ This is a Melos monorepo with the following packages: ``` packages/ - core/ # Rendering primitives, Markdown parsing, schema validation (Dart-only) + core/ # Shared deck models, schemas, Markdown utilities, storage contracts (Dart-only) superdeck/ # Flutter widgets and presentation components - cli/ # superdeck CLI tool (setup, build, publish, version) - builder/ # Code generators and build_runner integration + cli/ # superdeck CLI tool (setup, build/watch, version) + builder/ # Deck parsing, serialization, and build/watch pipeline (Dart-only) + playground/ # Flutter authoring app, editor, and AI generation + plugins/pdf/ # PDF export plugin demo/ # Sample presentation app docs/ # User-facing documentation (MDX format) ``` ### Key Package Responsibilities -- **core**: Markdown processing, slide/block configuration, shared model/schema validation, YAML utilities (no Flutter dependency) +- **core**: Shared Markdown utilities, slide/block configuration, model/schema validation, storage contracts, and YAML utilities (no Flutter dependency) - **superdeck**: Flutter widgets, DeckController, navigation, thumbnail/capture services, theme system - **cli**: CLI commands for project setup and building slides -- **builder**: build_runner generators for code generation +- **builder**: Markdown deck parsing, serialization, build/watch orchestration, and build plugins +- **playground**: Deck authoring, editor/file sessions, AI generation, and theme customization +- **plugins/pdf**: PDF capture and export UI ## Environment Setup @@ -52,7 +56,6 @@ Always work inside the FVM-provided SDK (`.fvm/flutter_sdk`) to avoid toolchain fvm dart run melos run analyze # Run dart analyze + DCM analysis fvm dart run melos run analyze:all # Full analysis including unused code/files fvm dart run melos run fix # Apply dart fix + DCM autofixes -fvm dart run melos run custom_lint_analyze # Run custom lint rules ``` ### Code Generation @@ -60,15 +63,44 @@ fvm dart run melos run custom_lint_analyze # Run custom lint rules fvm dart run melos run build_runner:build # Generate code (run before tests) fvm dart run melos run build_runner:watch # Watch mode for development fvm dart run melos run build_runner:clean # Clean generated files +fvm dart run melos run brb # Alias for build_runner:build +fvm dart run melos run brbc # Alias for build_runner:clean ``` ### Testing + +Each command covers one layer. No single command covers every layer. + ```bash -fvm dart run melos run test # Run all tests -fvm dart run melos run test:coverage # Run tests with coverage -fvm flutter test # Run specific test file +fvm dart run melos run test # Package unit and widget tests +fvm dart run melos run test:integration # Desktop integration tests (Linux) +fvm dart run melos run test:integration:macos # Desktop integration tests (macOS) +fvm dart run melos run test:e2e:web # Browser smoke tests (Chromium, WebKit) +fvm dart run melos run test:e2e # Linux integration + browser smoke +fvm dart run melos run test:all # Package tests + Linux integration tests +fvm dart run melos run test:coverage # Package tests with coverage +fvm flutter test # One test file ``` +`melos run test` excludes `ci-excluded` suites; `test:coverage` includes them. +No melos command runs the live generation tests. Run those from +`packages/playground`: + +```bash +# Deterministic checkpoint, no provider call. +fvm flutter test test_live/ai_generation/ai_generation_smoke_test.dart \ + --dart-define=LIVE_FAKE_CHECKPOINT=true --reporter expanded + +# Live 10-slide smoke test; requires the repository-root .env file. +fvm flutter test test_live/ai_generation/ai_generation_smoke_test.dart \ + --dart-define=LIVE_FIXTURE=superdeck_demo_10 \ + --dart-define-from-file=../../.env --reporter expanded +``` + +The live cases skip when the supplied defines contain no `GOOGLE_AI_API_KEY`. +A missing define file fails before the tests start. Omitting `LIVE_FIXTURE` +selects the default small-fixture suite rather than the 10-slide checkpoint. + ### Running Apps & Live Debugging ```bash cd packages/playground @@ -104,7 +136,7 @@ fvm dart run melos run clean # Clean all Flutter build artifacts - Two-space Dart indentation - `snake_case.dart` filenames - Prefer relative imports over package imports -- Avoid exporting from entry-point files +- Avoid importing the package's own entry-point file internally; import the defining file - Keep widgets focused; colocate private helpers with their widget - Run `melos run fix` before committing @@ -159,7 +191,7 @@ lib/src/ - Unit tests live under each package's `test/` directory - Always regenerate code before running tests - Add regression tests with bug fixes -- CI blocks merges on failing analyze/test jobs +- Require passing analysis and relevant test checks before merging ## Commit Guidelines @@ -219,6 +251,8 @@ Styles are defined in Dart through `SlideStyler`, `DeckOptions.baseStyle`, and ` | Bootstrap workspace | `fvm dart run melos bootstrap` | | Run all analysis | `fvm dart run melos run analyze` | | Generate code | `fvm dart run melos run build_runner:build` | -| Run tests | `fvm dart run melos run test` | +| Run package tests | `fvm dart run melos run test` | +| Run desktop integration tests | `fvm dart run melos run test:integration:macos` | +| Run browser smoke tests | `fvm dart run melos run test:e2e:web` | | Apply fixes | `fvm dart run melos run fix` | | Clean workspace | `fvm dart run melos run clean` | diff --git a/docs/generation_and_rendering_review_plan.md b/docs/generation_and_rendering_review_plan.md new file mode 100644 index 00000000..01a7e9c6 --- /dev/null +++ b/docs/generation_and_rendering_review_plan.md @@ -0,0 +1,90 @@ +# Remaining Markdown review work + +Updated September 8, 2026. The generation and runtime cleanup is implemented +in the PR stack below. The Markdown renderer comparison and parser assembly +review remain outstanding in this workspace, so this plan is still needed. + +This replaces the original broad review proposal with its remaining work. +The separate renderer handoff contains the detailed architecture and source +context. No Markdown implementation change is part of the cleanup stack. + +## Completed cleanup + +| Area | Implementation | +| --- | --- | +| Capture teardown, thumbnail pruning, transition ownership, and CI for stacked PRs | [PR #108](https://github.com/conceptadev/superdeck/pull/108) | +| Shared generation application, cancellation and document/file revision guards, service setup, and editor notices | [PR #109](https://github.com/conceptadev/superdeck/pull/109) | +| Build-status ownership, loader snapshots, script aliases, and test-command documentation | [PR #110](https://github.com/conceptadev/superdeck/pull/110) | + +These changes retain the existing package boundaries and generation models. +Generation has passed the deterministic checkpoint and a live 10-slide run +with three artworks, 18.778 seconds of generation time, and ten captured slides. +That validates one generation run; it does not establish renderer parity or +replace the separate interactive authoring checks. + +## 1. Establish the Markdown behavior contract + +Inventory the behavior supplied by the existing renderer and each custom +builder. Start with the +[Markdown showcase](../packages/core/test/fixtures/markdown/github_web_markdown_showcase.md) +and existing parser, codec, widget, and capture tests. + +Cover headings, paragraphs, inline formatting, nested lists, tasks, tables, +alerts, blockquotes, fenced code, Mermaid, images, custom widgets, Unicode, +text scaling, and overflow. Define intended link activation, selection, and +inline-image behavior before changing those features. + +Deliver a feature-to-test matrix that distinguishes current behavior from +approved changes. Reuse existing fixtures and assertions. + +## 2. Compare rendering the existing AST with ordinary widgets + +Keep the Dart `markdown` parser and the current `flutter_markdown_plus` +implementation as the working baseline. Begin the separate comparison with +paragraphs, headings, and nested alerts rendered from existing parsed nodes. +Preserve Mix styling, inline formatting, Hero behavior, asset resolution, and +syntax configuration. Do not introduce another AST or a permanent renderer +selection framework for the experiment. + +Compare the maintained code each approach needs, parser invocations during +style changes, cold/warm render cost, transition frames, capture completion, +and memory for representative 10- and 20-slide decks. Use matching SDK, fonts, +assets, devices, build modes, and cache conditions. + +A successful text prototype is not a complete renderer. Before selecting or +extending it, evaluate lists, tables, code, blockquotes, images, semantics, +links, and selection on macOS and web. Investigate another widget package only +if the comparison leaves a specific requirement unresolved. + +Deliver a keep, simplify, or replace decision supported by screenshots, +interaction results, measurements, and maintenance cost. A replacement needs +an identifiable benefit and coverage of every affected contract. + +## 3. Review shared deck assembly + +The CLI builder and Playground codec still assemble slide models separately +from the same deck parsers. Compare both paths before plugins run, using the +same inputs: options, sections, comments, widget arguments, slide identity, +fenced directives, malformed edits, and error diagnostics. + +If parity supports consolidation, extract one small pure assembly function in +`builder`. Keep filesystem access, plugins, status reporting, and each caller's +recovery behavior with their current owners. Preserve serializer normalization +and slide-identity contracts. This is a separate parser change from selecting +a content renderer. + +## Validation and retirement + +Use the layer-specific commands in [AGENTS.md](../AGENTS.md). Regenerate code, +run analysis and contract checks, then run affected package, macOS integration, +and Chromium/WebKit tests. Capture output is part of renderer acceptance. + +For each Markdown change, verify the complete authoring path: CLI build, +opening a deck, editing, invalid-edit recovery, saving/reloading, navigation, +and thumbnail/PDF capture. Keep `fvm flutter run` attached during manual checks +and inspect its logs after interactions. Run live generation again when its +behavior or model configuration changes. + +Retire this plan when the renderer decision and parser consolidation have +been implemented or explicitly declined, with the decisions and validation +recorded in their PRs or maintained architecture documentation. diff --git a/packages/builder/lib/src/build/deck_builder.dart b/packages/builder/lib/src/build/deck_builder.dart index 09bcbba5..5f4b1b0c 100644 --- a/packages/builder/lib/src/build/deck_builder.dart +++ b/packages/builder/lib/src/build/deck_builder.dart @@ -81,11 +81,7 @@ class DeckBuilder { final slides = await build(); yield BuildCompleted(slides.toList()); } catch (e, stackTrace) { - await store.saveBuildStatus( - phase: DeckBuildPhase.failure, - error: e, - stackTrace: stackTrace, - ); + // [build] already published the failure status for this build. yield BuildFailed(e, stackTrace); } } @@ -103,7 +99,37 @@ class DeckBuilder { return buildFuture; } + /// Runs one build and publishes its status. + /// + /// This is the single owner of the build status, so a direct build and a + /// watch build report a failure the same way. The original build error + /// always reaches the caller, even when the status write fails too. Future> _build() async { + try { + return await _runBuild(); + } catch (error, stackTrace) { + await _publishBuildFailure(error, stackTrace); + rethrow; + } + } + + Future _publishBuildFailure(Object error, StackTrace stackTrace) async { + try { + await store.saveBuildStatus( + phase: DeckBuildPhase.failure, + error: error, + stackTrace: stackTrace, + ); + } catch (statusError, statusStackTrace) { + _logger.warning( + 'Could not record the failed build status.', + statusError, + statusStackTrace, + ); + } + } + + Future> _runBuild() async { _logger.info('Starting build...'); await store.initialize(); await store.saveBuildStatus(phase: DeckBuildPhase.building); diff --git a/packages/builder/test/src/build/deck_builder_test.dart b/packages/builder/test/src/build/deck_builder_test.dart index c38cc108..d2a68faf 100644 --- a/packages/builder/test/src/build/deck_builder_test.dart +++ b/packages/builder/test/src/build/deck_builder_test.dart @@ -496,6 +496,90 @@ Discuss release plan. ); }); + test('publishes failure status for a direct build', () async { + const markdown = '# First Slide\n\nOriginal content'; + final builder = DeckBuilder( + workspace: workspace, + store: store, + plugins: [ + _TransformPlugin( + id: 'test.direct-failure', + transform: (_, _) => throw StateError('transform failed'), + ), + ], + ); + + await workspace.slidesFile.writeAsString(markdown); + await expectLater(() => builder.build(), throwsA(isA())); + + final status = await _readBuildStatus(workspace); + expect(status.phase, DeckBuildPhase.failure); + expect(status.error?.message, contains('transform failed')); + }); + + test('a later successful build replaces the failure status', () async { + const markdown = '# First Slide\n\nOriginal content'; + var shouldFail = true; + final builder = DeckBuilder( + workspace: workspace, + store: store, + plugins: [ + _TransformPlugin( + id: 'test.recovering', + transform: (block, _) { + if (shouldFail) throw StateError('transform failed'); + + return block; + }, + ), + ], + ); + + await workspace.slidesFile.writeAsString(markdown); + await expectLater(() => builder.build(), throwsA(isA())); + expect((await _readBuildStatus(workspace)).phase, DeckBuildPhase.failure); + + shouldFail = false; + await builder.build(); + + final status = await _readBuildStatus(workspace); + expect(status.phase, DeckBuildPhase.success); + expect(status.slideCount, 1); + expect(status.error, isNull); + }); + + test('keeps the build error when the status write also fails', () async { + const markdown = '# First Slide\n\nOriginal content'; + final failingStore = _FailingStatusStore( + workspace: workspace, + failOn: DeckBuildPhase.failure, + ); + final builder = DeckBuilder( + workspace: workspace, + store: failingStore, + plugins: [ + _TransformPlugin( + id: 'test.status-write-failure', + transform: (_, _) => throw StateError('transform failed'), + ), + ], + ); + + await workspace.slidesFile.writeAsString(markdown); + + await expectLater( + () => builder.build(), + throwsA( + isA().having( + (error) => error.toString(), + 'message', + contains('transform failed'), + ), + ), + ); + expect(failingStore.failureWriteAttempts, 1); + }); + test('preserves DeckFormatException thrown by plugins', () async { const markdown = '# First Slide\n\nOriginal content'; final builder = DeckBuilder( @@ -643,3 +727,39 @@ final class _FailingReferenceStore extends DeckBuildStore { await onSaveReferences(); } } + +Future _readBuildStatus(DeckWorkspace workspace) async { + final decoded = + jsonDecode(await workspace.buildStatusJson.readAsString()) + as Map; + + return DeckBuildStatus.fromJson(decoded); +} + +/// Store that refuses to record one build phase. +final class _FailingStatusStore extends DeckBuildStore { + _FailingStatusStore({required super.workspace, required this.failOn}); + + final DeckBuildPhase failOn; + var failureWriteAttempts = 0; + + @override + Future saveBuildStatus({ + required DeckBuildPhase phase, + int? slideCount, + Object? error, + StackTrace? stackTrace, + }) async { + if (phase == failOn) { + failureWriteAttempts++; + throw const FileSystemException('Cannot write the build status.'); + } + + return super.saveBuildStatus( + phase: phase, + slideCount: slideCount, + error: error, + stackTrace: stackTrace, + ); + } +} diff --git a/packages/builder/test/src/build/deck_builder_watch_test.dart b/packages/builder/test/src/build/deck_builder_watch_test.dart index a27a1a5e..50916431 100644 --- a/packages/builder/test/src/build/deck_builder_watch_test.dart +++ b/packages/builder/test/src/build/deck_builder_watch_test.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:superdeck_builder/src/build/build_event.dart'; @@ -73,6 +74,25 @@ void main() { ); }); + test('publishes failure status for a watch build', () async { + await workspace.slidesFile.writeAsString('# Slide\n\n@column\n'); + + final builder = DeckBuilder(workspace: workspace, store: store); + final iterator = StreamIterator(builder.watchAndBuild()); + addTearDown(iterator.cancel); + + await _nextEvent(iterator); + expect(await _nextEvent(iterator), isA()); + + final decoded = + jsonDecode(await workspace.buildStatusJson.readAsString()) + as Map; + final status = DeckBuildStatus.fromJson(decoded); + + expect(status.phase, DeckBuildPhase.failure); + expect(status.error, isNotNull); + }); + test('emits started and completed for a rebuild cycle', () async { final builder = DeckBuilder(workspace: workspace, store: store); final iterator = StreamIterator(builder.watchAndBuild()); diff --git a/packages/cli/lib/src/commands/build_command.dart b/packages/cli/lib/src/commands/build_command.dart index 8c3177f1..d5317be3 100644 --- a/packages/cli/lib/src/commands/build_command.dart +++ b/packages/cli/lib/src/commands/build_command.dart @@ -72,7 +72,8 @@ class BuildCommand extends SuperDeckCommand { /// Runs the build process with proper error handling and progress reporting. /// /// Uses the provided [builder] for the build, or creates a new one if not - /// provided. + /// provided. The builder owns the build status, so this method only reports + /// the failure to the console. Future _runBuild( DeckBuildStore store, DeckWorkspace workspace, { @@ -113,31 +114,16 @@ class BuildCommand extends SuperDeckCommand { progress.fail('Build failed'); logger.err('File system error: ${e.message}'); logger.err('Path: ${e.path ?? 'Unknown'}'); - await store.saveBuildStatus( - phase: .failure, - error: e, - stackTrace: .current, - ); return false; } on FormatException catch (e) { progress.fail('Format error'); logger.err(e.message); - await store.saveBuildStatus( - phase: .failure, - error: e, - stackTrace: .current, - ); return false; } catch (e, stackTrace) { progress.fail('Build failed'); _logBuildFailure(e, stackTrace); - await store.saveBuildStatus( - phase: .failure, - error: e, - stackTrace: stackTrace, - ); return false; } finally { @@ -284,6 +270,8 @@ class BuildCommand extends SuperDeckCommand { return ExitCode.success.code; } catch (e, stackTrace) { + // Only failures raised before a builder runs are reported here. Once a + // builder exists it publishes its own failure status. logger.err('Build failed before the deck could be generated.'); _logBuildFailure(e, stackTrace); await store?.saveBuildStatus( diff --git a/packages/superdeck/lib/src/deck/loaders/file_deck_loader.dart b/packages/superdeck/lib/src/deck/loaders/file_deck_loader.dart index fac26f30..4ec67cb3 100644 --- a/packages/superdeck/lib/src/deck/loaders/file_deck_loader.dart +++ b/packages/superdeck/lib/src/deck/loaders/file_deck_loader.dart @@ -62,14 +62,18 @@ class FileDeckLoader extends DeckLoader { return _statusFile.readAsString(); } - Future _processStatus(Completer cancel) async { - if (!await _statusFile.exists()) { + /// Emits the events for one already-read status snapshot. + /// + /// The caller passes the snapshot it compares later, so the loader cannot + /// process content that the build replaced after that comparison started. + Future _processStatus(String? snapshot, Completer cancel) async { + if (snapshot == null) { _emitMissingBuildOutput(cancel); return; } try { - final decoded = jsonDecode(await _statusFile.readAsString()); + final decoded = jsonDecode(snapshot); if (decoded is! Map) { _emit( SlidesErrorEvent( @@ -187,7 +191,7 @@ class FileDeckLoader extends DeckLoader { } final statusAtStart = await _readStatusSnapshot(); - await _processStatus(cancel); + await _processStatus(statusAtStart, cancel); if (!_isCycleActive(cancel)) return; final changed = await _waitForStatusChange( diff --git a/packages/superdeck/test/src/deck/loaders/file_deck_loader_snapshot_test.dart b/packages/superdeck/test/src/deck/loaders/file_deck_loader_snapshot_test.dart new file mode 100644 index 00000000..5a8ab567 --- /dev/null +++ b/packages/superdeck/test/src/deck/loaders/file_deck_loader_snapshot_test.dart @@ -0,0 +1,89 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:superdeck/superdeck.dart'; +import 'package:superdeck_core/superdeck_core.dart'; + +/// Status file whose reads follow a script, so the loader can be observed +/// while the build replaces the status between two reads. +class _ScriptedStatusFile implements File { + _ScriptedStatusFile(this._delegate, this._contents); + + final File _delegate; + final List _contents; + var reads = 0; + + @override + String get path => _delegate.path; + + @override + Directory get parent => _delegate.parent; + + @override + Future exists() => _delegate.exists(); + + @override + Future readAsString({Encoding encoding = utf8}) async { + final index = reads < _contents.length ? reads : _contents.length - 1; + reads++; + + return _contents[index]; + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +String _statusJson(String status, int seq) => + '{"status":"$status","timestamp":"2026-03-10T10:00:0$seq.000Z"}'; + +void main() { + test('processes the status snapshot it compares against', () async { + final tempDir = await Directory.systemTemp.createTemp( + 'superdeck_loader_snapshot_', + ); + addTearDown(() async { + if (await tempDir.exists()) await tempDir.delete(recursive: true); + }); + + final workspace = DeckWorkspace(projectDir: tempDir.path); + await workspace.superdeckDir.create(recursive: true); + await workspace.deckJson.writeAsString('[]'); + final realStatusFile = workspace.buildStatusJson; + await realStatusFile.writeAsString(_statusJson('building', 1)); + + // The second read returns the finished build, which is what an + // unprotected second read inside one cycle would observe. + final scriptedStatusFile = _ScriptedStatusFile(realStatusFile, [ + _statusJson('building', 1), + _statusJson('success', 2), + ]); + + final events = []; + await IOOverrides.runZoned( + () async { + final deckLoader = FileDeckLoader(workspace: workspace); + final subscription = deckLoader.load().listen(events.add); + final deadline = DateTime.now().add(const Duration(seconds: 3)); + while (events.length < 3 && DateTime.now().isBefore(deadline)) { + await Future.delayed(const Duration(milliseconds: 20)); + } + await subscription.cancel(); + await deckLoader.dispose(); + }, + // `File(path)` inside this callback would consult the same override + // again, so unrelated paths resolve in the root zone. + createFile: (path) => path == realStatusFile.path + ? scriptedStatusFile + : Zone.root.run(() => File(path)), + ); + + expect(events, hasLength(greaterThanOrEqualTo(3))); + expect(events[0], isA()); + // The first cycle must emit the snapshot it read, not a later one. + expect(events[1], isA()); + expect(events[2], isA()); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index 1e5a034e..145982ac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -115,17 +115,24 @@ melos: test: run: fvm dart run melos exec -c 1 -- fvm flutter test --exclude-tags ci-excluded - description: Run flutter tests excluding explicit golden and flaky suites. + description: >- + Run the package unit and widget tests. Excludes `ci-excluded` suites, + desktop integration tests, browser smoke tests, and live generation + tests. packageFilters: dirExists: test test:integration: run: cd demo && fvm dart run superdeck_cli:main build && fvm flutter test integration_test/all_tests.dart -d linux --fail-fast --timeout 2m - description: Run flutter integration tests on Linux (CI default) + description: >- + Run the demo desktop integration tests on Linux, which is the CI + default device. test:integration:macos: run: cd demo && fvm dart run superdeck_cli:main build && fvm flutter test integration_test/all_tests.dart -d macos --fail-fast --timeout 2m - description: Run flutter integration tests on macOS (local) + description: >- + Run the demo desktop integration tests on macOS, which is the local + default device. test:e2e:web:prepare: run: cd demo && fvm dart run superdeck_cli:main build && fvm flutter build web --release @@ -133,15 +140,19 @@ melos: test:e2e:web: run: fvm dart run melos run test:e2e:web:prepare && cd demo/e2e && npm ci && npx playwright install --with-deps chromium webkit && npm run test:smoke - description: Run Playwright smoke tests for the demo web build on Chromium and WebKit. + description: >- + Run the browser smoke tests for the demo web build on Chromium and + WebKit. test:e2e: run: fvm dart run melos run test:integration && fvm dart run melos run test:e2e:web - description: Run full desktop integration and web smoke E2E coverage. + description: Run the desktop integration tests and the browser smoke tests. test:all: run: fvm dart run melos run test && fvm dart run melos run test:integration - description: Run all tests (unit + integration) + description: >- + Run the package tests and the Linux desktop integration tests. Does not + run the browser smoke tests or the live generation tests. test:coverage: run: fvm dart run melos exec -- fvm flutter test --coverage @@ -154,10 +165,9 @@ melos: description: Clean all packages brb: - run: fvm dart run melos run gen:build:superdeck + run: fvm dart run melos run build_runner:build + description: Short alias for `build_runner:build`. brbc: - run: fvm dart run melos run gen:clean - - custom_lint_analyze: - run: fvm dart run melos exec --depends-on=mix_lint -- fvm dart run custom_lint + run: fvm dart run melos run build_runner:clean + description: Short alias for `build_runner:clean`.