Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions .claude/rules/cli.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
---
path: "{lib/src/cli/**/*.dart,bin/**/*.dart}"
path: "{lib/src/cli/**/*.dart}"
---

# CLI Domain

- Commands extend `Command` from `magic_cli` — implement `name`, `description`, `configure(ArgParser)`, `handle()`
- `configure()` registers flags/options via `ArgParser` — `addFlag()` for booleans, `addOption()` for strings, `addMultiOption()` for lists
- `handle()` is `Future<void>` — read args from `arguments['key']`, cast to expected type
- Entry point (`bin/magic_notifications.dart`): create `Kernel()`, `registerMany([commands])`, `await kernel.handle(args)`
- CLI barrel (`lib/src/cli/cli.dart`): re-export `magic_cli` with `hide InstallCommand` to avoid name clash, export own commands
- File operations: use `FileHelper.findProjectRoot()`, `FileHelper.readFile()`, `FileHelper.writeFile()`, `FileHelper.fileExists()`
- Stub loading: `StubLoader.load('install/notification_config', searchPaths: paths)` — searches `assets/stubs/` directory
- Code injection: `ConfigEditor.addImportToFile()`, `ConfigEditor.insertCodeBeforePattern()` — idempotent (check before inserting)
- Testability: make `getProjectRoot()` and `getStubSearchPaths()` overridable methods — test subclasses override to use temp dirs
Commands are built on `fluttersdk_artisan` and surface via the host app's unified `artisan` binary.

## Command shape

- Commands extend `ArtisanCommand` — implement `signature` (DSL: `'name {arg} {--flag}'`), `description`, `boot` (return `CommandBoot.none`), `Future<int> handle(ArtisanContext ctx)`
- `signature` defines arguments and options; `ctx.argument('name')` and `ctx.option('flag')` retrieve values
- `handle()` returns exit code: `0` for success, `1` for error; use `ctx.output.error(msg)` for diagnostic messaging
- Install extends `ArtisanInstallCommand` — drives manifest-based install via `install.yaml` + transactional `PluginInstaller`; fluent override adds dynamic logic (UUID validation, conditional prompts, arbitrary file writes)

## Install infrastructure

- Manifest: `install.yaml` at package root (schema: `plugin_name`, `magic.provider`, `publish{stub:target}`, `native.*`, `post_install`)
- Transactional ops: `installer.writeFile(path, content)` (atomic, rolled back on error), then helper-backed mutations (`ConfigEditor`, `HtmlEditor`, `XmlEditor`, `MainDartEditor`) (synchronous, no rollback)
- Stub loading: `StubLoader.load('name', searchPaths)` — searches `assets/stubs/` directory; returns string content
- Code injection: `ConfigEditor.addImportToFile(path, import)`, `ConfigEditor.injectProvider(path, provider)` — idempotent (checks existing pattern before inserting)
- Idempotent re-install: guard helper ops with `HtmlEditor.hasContent(path, marker)` to avoid double-injection
- Testability: override `getProjectRoot()` and `getStubSearchPaths()` in test subclasses to use temp dirs

## MCP tools

`MagicNotificationsArtisanProvider.mcpTools()` exposes two read-only diagnostic tools:
- `notifications_doctor` — health check (config presence, OneSignal App ID format, polling interval, platform setup)
- `notifications_channels` — list channel status and configuration

Mutating commands (install, configure, test, uninstall, publish) are excluded from the MCP surface.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

## [Unreleased]

### Breaking Changes
- **Removed `bin/magic_notifications.dart` entrypoint** — CLI commands now surface via host app's artisan binary (`dart run <app>:artisan notifications:<cmd>`), not as a standalone `dart run magic_notifications <cmd>`. Update your scripts and CI workflows accordingly.
- **Removed `magic_cli` dependency** — Install now uses `fluttersdk_artisan`'s manifest-driven model, not magic_cli's imperative Kernel.

### Changed
- **Install now manifest-driven** — `install.yaml` declares the static slice (provider injection only); dynamic logic (UUID validation, platform conditionals, placeholder-rendered config, arbitrary file writes) lives in `InstallCommand`'s fluent override.
- **CLI architecture** — Commands contributed via `MagicNotificationsArtisanProvider` registered in host's `artisan.providers` config.

### Added
- **MCP tools** — `notifications_doctor` and `notifications_channels` are now read-only tools available to AI agents via MCP.

### 📚 Documentation
- **README**: Rewrite to match Magic ecosystem format
- **doc/ folder**: Add comprehensive documentation
Expand Down
25 changes: 15 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ Flutter push and database notification plugin for the Magic Framework. In-app po

## Commands

Notifications CLI commands surface via the host app's artisan binary (not as a standalone `magic_notifications` binary).

| Command | Description |
|---------|-------------|
| `flutter test --coverage` | Run all tests with coverage |
| `flutter analyze --no-fatal-infos` | Static analysis |
| `dart format .` | Format all code |
| `dart run magic_notifications install` | Interactive setup wizard |
| `dart run magic_notifications configure` | Update notification config |
| `dart run magic_notifications doctor` | Health check |
| `dart run magic_notifications test` | Send test notification |
| `dart run magic_notifications channels` | List channel status |
| `dart run magic_notifications uninstall` | Remove plugin integration |
| `dart run magic_notifications publish` | Copy config stub to project |
| `dart run <app>:artisan notifications:install` | Interactive setup wizard |
| `dart run <app>:artisan notifications:configure` | Update notification config |
| `dart run <app>:artisan notifications:doctor` | Health check (also available as MCP tool) |
| `dart run <app>:artisan notifications:test` | Send test notification |
| `dart run <app>:artisan notifications:channels` | List channel status (also available as MCP tool) |
| `dart run <app>:artisan notifications:uninstall` | Remove plugin integration |
| `dart run <app>:artisan notifications:publish` | Copy config stub to project |

## Architecture

Expand All @@ -38,12 +40,15 @@ lib/
├── providers/ # NotificationServiceProvider (register + boot)
├── widgets/ # PushPromptDialog
├── exceptions/ # NotificationException
└── cli/commands/ # install, configure, doctor, test, channels, uninstall, publish
bin/
└── magic_notifications.dart # CLI entry point
└── cli/
├── notifications_artisan_provider.dart # MagicNotificationsArtisanProvider (7 commands + 2 MCP tools)
└── commands/ # InstallCommand, ConfigureCommand, DoctorCommand, TestCommand, ChannelsCommand, UninstallCommand, PublishCommand
install.yaml # Manifest-driven install configuration
assets/stubs/ # Stub templates for code generation
```

**CLI architecture:** Notifications commands are contributed via `MagicNotificationsArtisanProvider` (extends `ArtisanServiceProvider`) registered in the host app's `artisan.providers` config. The provider exposes 7 commands (`notifications:install/configure/test/doctor/uninstall/publish/channels`) and 2 read-only MCP tools (`notifications_doctor`, `notifications_channels`). Install is hybrid: static layer in `install.yaml` (provider injection only), dynamic layer in `InstallCommand`'s fluent override (UUID validation, platform conditionals, placeholder-rendered config, arbitrary file writes). No standalone `bin/` entry point; commands surface through the host app's unified artisan binary.

**Data flow:** App boot → `NotificationServiceProvider.boot()` → registers channels + push driver → `Notify.startPolling()` → `NotificationPoller` fetches via HTTP → emits to broadcast stream. Push: `Notify.initializePush()` → OneSignalDriver → platform-specific (mobile vs web JS interop)

**Pure Dart** — no android/, ios/, or native platform code. Platform support via `onesignal_flutter` package.
Expand Down
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ dependencies:
### 2. Install configuration

```bash
dart run magic_notifications install
dart run <app>:artisan notifications:install
```

This generates `lib/config/notifications.dart`, injects `NotificationServiceProvider` into `lib/config/app.dart`, wires the `notificationConfig` factory into `lib/main.dart`, and configures platform-specific setup for your selected platforms.
Expand Down Expand Up @@ -164,17 +164,19 @@ Future<void> onLogout() async {

## CLI Tools

All commands use the single entry point `dart run magic_notifications [command]`:
All commands use the host app's artisan binary: `dart run <app>:artisan notifications:[command]`

| Command | Description |
|---------|-------------|
| `install` | Interactive wizard to set up notifications |
| `configure` | Update notification configuration |
| `doctor` | Check installation and configuration health |
| `test` | Send test notifications to verify setup |
| `channels` | List all channels and their status |
| `publish` | Copy config stub to your project |
| `uninstall` | Remove plugin integration |
| `notifications:install` | Interactive wizard to set up notifications |
| `notifications:configure` | Update notification configuration |
| `notifications:doctor` | Check installation and configuration health |
| `notifications:test` | Send test notifications to verify setup |
| `notifications:channels` | List all channels and their status |
| `notifications:publish` | Copy config stub to your project |
| `notifications:uninstall` | Remove plugin integration |

The `notifications:doctor` and `notifications:channels` commands are also available as read-only MCP tools for AI agents.

See the [CLI Reference](https://magic.fluttersdk.com/packages/notifications/basics/cli) for all flags and options.

Expand Down
28 changes: 0 additions & 28 deletions bin/magic_notifications.dart

This file was deleted.

68 changes: 31 additions & 37 deletions install.yaml
Original file line number Diff line number Diff line change
@@ -1,52 +1,46 @@
# magic_notifications install manifest (PluginInstaller DSL schema v1).
#
# Notifications is a Magic-side plugin: install registers
# NotificationsArtisanProvider into the consumer's lib/app/_plugins.g.dart
# codegen barrel AND injects NotificationServiceProvider into
# lib/config/app.dart's providers list so the Magic IoC container resolves
# the runtime Notifications facade.
# Hybrid layout: this manifest carries only the STATIC, always-on slice. The
# dynamic OneSignal install flow lives in InstallCommand's fluent override,
# because the v1 schema cannot express any of:
#
# No stub files are published: push driver config is scaffolded on demand
# by `dart run artisan notifications:configure --driver=<onesignal|fcm>`.
# - fail-fast UUID validation of --app-id before staging,
# - prompts that only fire when web is selected (manifest `prompts:` run
# unconditionally during prepare()),
# - an arbitrary web/ file write (native.web supports only head_scripts /
# meta_tags), or
# - a head-script injection guarded for idempotency (the head_scripts
# dispatcher has no hasContent guard, so a re-install would double-inject).
#
# The provider injection IS static (the consumer's lib/config/app.dart provider
# list is mutated unconditionally on every install), so its provider name stays
# here as `magic.provider`. InstallCommand reads that name, then drives the whole
# staging itself: it stages the transactional writeFile ops FIRST (so the atomic
# .tmp swap covers them), and issues the helper-backed provider inject (plus the
# other synchronous mutations) LAST. The provider inject is therefore the last
# op, not a step the manifest stages before the override runs.
#
# See doc/install_yaml_schema.md (in fluttersdk_artisan) for the full schema.

plugin_name: magic_notifications

publish: {}

magic:
# Injects `import 'package:magic_notifications/magic_notifications.dart';`
# plus `(app) => NotificationServiceProvider(app),` into the consumer's
# lib/config/app.dart providers list. Idempotent (ConfigEditor skips when the
# import + entry already exist), so re-running install does not duplicate it.
provider: NotificationServiceProvider

post_install:
message: |
magic_notifications plugin:installed. Bootstrap workflow:

1. Verify the IoC wiring (auto-injected by `magic:install`):

lib/config/app.dart:
providers: [
...,
(app) => NotificationServiceProvider(app),
],

2. Configure your push driver (OneSignal, FCM, or null for dev):

dart run artisan notifications:configure --driver=onesignal

3. Run doctor to verify the driver SDK is wired and credentials resolve:

dart run artisan notifications:doctor

4. Send a test notification:

dart run artisan notifications:publish --to=<user-id> --title='Test'

5. Manage push channels (subscribe / unsubscribe / topic broadcast):

dart run artisan notifications:channels --list
Magic Notifications installed. Next steps:

6. Verify the CLI surface:
1. Run `flutter pub get`.
2. Configure your OneSignal dashboard.
3. Verify the install: dart run <app>:artisan notifications:doctor
4. Manage channels: dart run <app>:artisan notifications:channels --list

dart run artisan list # includes notifications:install + configure
# + doctor + publish + channels + uninstall
Note: provider + main.dart wiring are written synchronously by the install
helpers and are NOT rolled back on a later failure. If install reports an
error after the provider was injected, inspect lib/config/app.dart and
lib/main.dart before re-running.
26 changes: 26 additions & 0 deletions lib/cli.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/// CLI barrel for Magic Notifications.
///
/// Exposes ONLY the artisan-CLI surface ([MagicNotificationsArtisanProvider]).
/// Does NOT export the Magic Notifications runtime (no Flutter, dart:ui, or
/// package:magic runtime imports), so this barrel is safe for consumption from
/// pure-Dart artisan dispatchers.
///
/// Consumers register the provider in their `bin/artisan.dart`:
///
/// ```dart
/// import 'package:fluttersdk_artisan/artisan.dart';
/// import 'package:magic_notifications/cli.dart' show MagicNotificationsArtisanProvider;
///
/// Future<void> main(List<String> args) async {
/// final registry = ArtisanRegistry()
/// ..registerProvider(MagicNotificationsArtisanProvider());
/// exit(await ArtisanApplication(registry: registry).dispatch(args));
/// }
/// ```
///
/// Runtime consumers (lib/main.dart of a Magic-based app) continue to
/// import `package:magic_notifications/magic_notifications.dart` for facades
/// and the full notification surface.
library;

export 'src/cli/notifications_artisan_provider.dart';
20 changes: 17 additions & 3 deletions lib/src/cli/cli.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
/// CLI utilities and helpers
/// CLI barrel for Magic Notifications.
///
/// Re-exports the shared CLI base infrastructure from fluttersdk_artisan.
/// Re-exports the artisan infrastructure plus the notifications provider and
/// all seven notifications:* commands. Consumers register
/// [MagicNotificationsArtisanProvider] in their `artisan.providers` config;
/// the commands surface automatically through the host [ArtisanApplication].
library;

export 'package:fluttersdk_artisan/artisan.dart';
// Hide the artisan-builtin DoctorCommand so the notifications-specific
// DoctorCommand (commands/doctor_command.dart) is the only one visible from
// this barrel.
export 'package:fluttersdk_artisan/artisan.dart' hide DoctorCommand;
export 'commands/channels_command.dart';
export 'commands/configure_command.dart';
export 'commands/doctor_command.dart';
export 'commands/install_command.dart';
export 'commands/publish_command.dart';
export 'commands/test_command.dart';
export 'commands/uninstall_command.dart';
export 'notifications_artisan_provider.dart';
4 changes: 2 additions & 2 deletions lib/src/cli/commands/channels_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:fluttersdk_artisan/artisan.dart';
///
/// ## Usage
/// ```bash
/// artisan notifications:channels
/// dart run <app>:artisan notifications:channels
/// ```
class ChannelsCommand extends ArtisanCommand {
@override
Expand Down Expand Up @@ -39,7 +39,7 @@ class ChannelsCommand extends ArtisanCommand {
if (!FileHelper.fileExists(configPath)) {
ctx.output.error('Configuration file not found.');
ctx.output.info(
'Run: ${ConsoleStyle.cyan}artisan notifications:install${ConsoleStyle.reset} to set up notifications.',
'Run: ${ConsoleStyle.cyan}dart run <app>:artisan notifications:install${ConsoleStyle.reset} to set up notifications.',
);
return 1;
}
Expand Down
3 changes: 2 additions & 1 deletion lib/src/cli/commands/configure_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ class ConfigureCommand extends ArtisanCommand {
// Check if config exists before proceeding with any operation.
if (!configExists()) {
ctx.output.error('Configuration file not found');
ctx.output.info('Run installation first: artisan notifications:install');
ctx.output.info(
'Run installation first: dart run <app>:artisan notifications:install');
return 1;
}

Expand Down
14 changes: 9 additions & 5 deletions lib/src/cli/commands/doctor_command.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'package:fluttersdk_artisan/artisan.dart';
// cli.dart re-exports fluttersdk_artisan/artisan.dart (hiding only the builtin
// DoctorCommand that collides with this class), so a direct artisan.dart import
// is redundant here.
import 'package:magic_notifications/src/cli/cli.dart';

/// Diagnostic command for checking Magic Notifications health.
Expand All @@ -11,8 +13,8 @@ import 'package:magic_notifications/src/cli/cli.dart';
///
/// ## Usage
/// ```bash
/// artisan notifications:doctor
/// artisan notifications:doctor --verbose
/// dart run <app>:artisan notifications:doctor
/// dart run <app>:artisan notifications:doctor --verbose
/// ```
class DoctorCommand extends ArtisanCommand {
@override
Expand Down Expand Up @@ -51,8 +53,10 @@ class DoctorCommand extends ArtisanCommand {
} else {
ctx.output.writeln('');
ctx.output.warning('Issues detected. Run the following to fix:');
ctx.output.writeln(' • Install: artisan notifications:install');
ctx.output.writeln(' • Configure: artisan notifications:configure');
ctx.output
.writeln(' • Install: dart run <app>:artisan notifications:install');
ctx.output.writeln(
' • Configure: dart run <app>:artisan notifications:configure');
return 1;
}
}
Expand Down
Loading