From 9daa42f9f77c6cee61dc7a8abfad973805e1754c Mon Sep 17 00:00:00 2001 From: keinstn Date: Wed, 29 Jul 2026 12:14:44 +0900 Subject: [PATCH 1/2] docs: install drover-notify with `herdr plugin install`, now that it is public MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `keinstn/drover-notify` is public as of today, so the workaround the setup guide was built around is gone. `herdr plugin install` only supports an unauthenticated HTTPS clone, which is exactly why the guide previously told people to `git clone` over SSH and `herdr plugin link` the checkout, and why it carried a "not yet available" note about the one-line path. The one-line path is now the default. Cloning and linking is kept as the alternative for anyone running a fork or testing a branch — it is a legitimate choice, not a workaround, so it reads that way now. The stated Herdr floor moves from 0.7.0 to **0.7.5**. 0.7.0 is what the plugin alone needs, and drover-notify's own README says so, but this document is read by drover users and drover's `kMinHerdrVersion` is `(0, 7, 5)`. Following the old text on 0.7.0 left you with working notifications from a drover that refuses to start agents. The note added under the install command explains the two floors so nobody lowers this one back to match the plugin's README. Left alone deliberately: README.md, which describes the plugin's location and its manual installation without ever claiming the repo is private. Installing is still deliberately a manual host operation — the app never installs executable code on the host. Separately, the in-app pairing dialog told users to "replace /path/to/drover" while the command shown directly beneath it reads `herdr plugin link /path/to/drover-notify`. The placeholder in the prose now matches the placeholder in the command, in both locales. Verified: `fvm flutter analyze` clean, `fvm flutter test` 790 passing. Co-Authored-By: Claude Opus 5 --- app/lib/l10n/app_en.arb | 2 +- app/lib/l10n/app_ja.arb | 2 +- app/lib/l10n/app_localizations.dart | 2 +- app/lib/l10n/app_localizations_en.dart | 2 +- app/lib/l10n/app_localizations_ja.dart | 2 +- docs/push-notifications.md | 26 ++++++++++++++++++-------- 6 files changed, 23 insertions(+), 13 deletions(-) diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 7e6dbb5..991ac4a 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -47,7 +47,7 @@ "notificationTargetUnavailable": "The agent from this notification is no longer available.", "hostPairNotifications": "Create notification pairing code", "hostPairingCodeTitle": "Pair the notification plugin", - "hostPairingCodeIntro": "On the Herdr host, copy and run these commands. Replace /path/to/drover with the checkout path. When setup asks, paste the pairing code.", + "hostPairingCodeIntro": "On the Herdr host, copy and run these commands. Replace /path/to/drover-notify with the plugin path. When setup asks, paste the pairing code.", "hostPairingLinkCommandLabel": "1. Link the plugin (first time only)", "hostPairingSetupCommandLabel": "2. Run setup", "hostPairingCodeLabel": "3. Pairing code", diff --git a/app/lib/l10n/app_ja.arb b/app/lib/l10n/app_ja.arb index e74cbf6..c757b9e 100644 --- a/app/lib/l10n/app_ja.arb +++ b/app/lib/l10n/app_ja.arb @@ -37,7 +37,7 @@ "notificationTargetUnavailable": "この通知のエージェントは利用できなくなりました。", "hostPairNotifications": "通知用のペアリングコードを作成", "hostPairingCodeTitle": "通知 plugin をペアリング", - "hostPairingCodeIntro": "Herdr host で、次のコマンドをコピーして実行してください。/path/to/drover は checkout のパスに置き換えます。setup が表示した prompt にペアリングコードを貼り付けてください。", + "hostPairingCodeIntro": "Herdr host で、次のコマンドをコピーして実行してください。/path/to/drover-notify は plugin のパスに置き換えます。setup が表示した prompt にペアリングコードを貼り付けてください。", "hostPairingLinkCommandLabel": "1. plugin を link(初回のみ)", "hostPairingSetupCommandLabel": "2. setup を実行", "hostPairingCodeLabel": "3. ペアリングコード", diff --git a/app/lib/l10n/app_localizations.dart b/app/lib/l10n/app_localizations.dart index 395e798..655864b 100644 --- a/app/lib/l10n/app_localizations.dart +++ b/app/lib/l10n/app_localizations.dart @@ -323,7 +323,7 @@ abstract class AppLocalizations { /// No description provided for @hostPairingCodeIntro. /// /// In en, this message translates to: - /// **'On the Herdr host, copy and run these commands. Replace /path/to/drover with the checkout path. When setup asks, paste the pairing code.'** + /// **'On the Herdr host, copy and run these commands. Replace /path/to/drover-notify with the plugin path. When setup asks, paste the pairing code.'** String get hostPairingCodeIntro; /// No description provided for @hostPairingLinkCommandLabel. diff --git a/app/lib/l10n/app_localizations_en.dart b/app/lib/l10n/app_localizations_en.dart index dbae0b5..c94849d 100644 --- a/app/lib/l10n/app_localizations_en.dart +++ b/app/lib/l10n/app_localizations_en.dart @@ -137,7 +137,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get hostPairingCodeIntro => - 'On the Herdr host, copy and run these commands. Replace /path/to/drover with the checkout path. When setup asks, paste the pairing code.'; + 'On the Herdr host, copy and run these commands. Replace /path/to/drover-notify with the plugin path. When setup asks, paste the pairing code.'; @override String get hostPairingLinkCommandLabel => diff --git a/app/lib/l10n/app_localizations_ja.dart b/app/lib/l10n/app_localizations_ja.dart index cf44b5a..2d32daa 100644 --- a/app/lib/l10n/app_localizations_ja.dart +++ b/app/lib/l10n/app_localizations_ja.dart @@ -127,7 +127,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get hostPairingCodeIntro => - 'Herdr host で、次のコマンドをコピーして実行してください。/path/to/drover は checkout のパスに置き換えます。setup が表示した prompt にペアリングコードを貼り付けてください。'; + 'Herdr host で、次のコマンドをコピーして実行してください。/path/to/drover-notify は plugin のパスに置き換えます。setup が表示した prompt にペアリングコードを貼り付けてください。'; @override String get hostPairingLinkCommandLabel => '1. plugin を link(初回のみ)'; diff --git a/docs/push-notifications.md b/docs/push-notifications.md index 6184c17..7c8d6cf 100644 --- a/docs/push-notifications.md +++ b/docs/push-notifications.md @@ -16,18 +16,28 @@ installs or updates executable code on the Herdr host. The plugin lives in its own repo, [`keinstn/drover-notify`](https://github.com/keinstn/drover-notify). The host -needs Herdr 0.7.0 or newer and Node.js 18 or newer. Clone the plugin repo onto -the host — it's private, so clone over SSH — then link it: +needs Herdr 0.7.5 or newer and Node.js 18 or newer. Install it on the host: ```sh -git clone git@github.com:keinstn/drover-notify.git -herdr plugin link /path/to/drover-notify +herdr plugin install keinstn/drover-notify ``` -Once `keinstn/drover-notify` is public, `herdr plugin install -keinstn/drover-notify` will replace the clone step above — **not yet -available** while the repo is private, since `herdr plugin install` only -supports an unauthenticated HTTPS clone. +`herdr plugin install` fetches over unauthenticated HTTPS, which is why this is +the one-line path now that the repo is public. + +drover-notify's own README says Herdr 0.7.0, which is right for the plugin by +itself. The floor above is drover's — `kMinHerdrVersion` in +`app/lib/src/herdr/herdr_version.dart` is `(0, 7, 5)` — because below it drover +still reads and steers agents but refuses to start them. **Do not lower this +number to match the plugin's README.** + +If you would rather run your own checkout — a fork, or a branch you are testing +— clone it and link that instead: + +```sh +git clone https://github.com/keinstn/drover-notify.git +herdr plugin link /path/to/drover-notify +``` ## Pair the plugin From 84e7d649049c4036b67fbcf6004a79d8a326b9fa Mon Sep 17 00:00:00 2001 From: keinstn Date: Wed, 29 Jul 2026 13:00:34 +0900 Subject: [PATCH 2/2] feat(app): install the notification plugin from GitHub in the pairing dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the second half of #122. The manual pairing dialog told the user to run `herdr plugin link /path/to/drover-notify` and then `node /path/to/drover-notify/bin/setup.mjs …`, where `/path/to/drover-notify` was a literal they had to find and substitute twice. That existed because `herdr plugin install` clones over unauthenticated HTTPS and could not reach a private repo. drover-notify is public now, so the one-liner works: herdr plugin install keinstn/drover-notify The setup command goes with it rather than being rewritten. `plugin install` clones from GitHub and puts the checkout where herdr chooses, so there is no path drover can print — and none is needed. This dialog only appears when `_detectPlugin` found nothing; once the plugin is installed, detection succeeds and pairing runs automatically, which is what the reworded intro now tells the user to do. The pairing code and completion URL stay on screen for anyone pairing by hand. `hostPairingLinkCommandLabel` and `hostPairingSetupCommandLabel` are replaced by `hostPairingInstallCommandLabel` and `hostPairingManualNote` in both locales. A widget test asserts the rendered dialog: the install command is present, and no `/path/to/`, `plugin link` or `setup.mjs` text survives anywhere in it. Mutation-checked — restoring the old `plugin link /path/to/drover-notify` command fails that test and only that test. Verified: `fvm flutter analyze` clean, `fvm flutter test` 791 passing. Co-Authored-By: Claude Opus 5 --- app/lib/l10n/app_en.arb | 8 +-- app/lib/l10n/app_ja.arb | 8 +-- app/lib/l10n/app_localizations.dart | 16 +++--- app/lib/l10n/app_localizations_en.dart | 10 ++-- app/lib/l10n/app_localizations_ja.dart | 9 +-- app/lib/src/screens/host_setup_screen.dart | 23 ++++---- app/test/screens/host_setup_screen_test.dart | 59 ++++++++++++++++++++ 7 files changed, 96 insertions(+), 37 deletions(-) diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 991ac4a..2b2118d 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -47,10 +47,10 @@ "notificationTargetUnavailable": "The agent from this notification is no longer available.", "hostPairNotifications": "Create notification pairing code", "hostPairingCodeTitle": "Pair the notification plugin", - "hostPairingCodeIntro": "On the Herdr host, copy and run these commands. Replace /path/to/drover-notify with the plugin path. When setup asks, paste the pairing code.", - "hostPairingLinkCommandLabel": "1. Link the plugin (first time only)", - "hostPairingSetupCommandLabel": "2. Run setup", - "hostPairingCodeLabel": "3. Pairing code", + "hostPairingCodeIntro": "Drover couldn't find the notification plugin on this host. Run this on the Herdr host to install it, then tap Create notification pairing code again — Drover will find it and pair for you.", + "hostPairingInstallCommandLabel": "Install the plugin", + "hostPairingManualNote": "Pairing by hand instead? The plugin's own setup script asks for the code and the URL below.", + "hostPairingCodeLabel": "Pairing code", "hostPairingUrlLabel": "Completion URL", "hostPairAutoDetectedTitle": "Notification plugin detected", "hostPairAutoDetectedBody": "Drover found the drover.notify plugin already linked on this host. Set up push notification pairing automatically?", diff --git a/app/lib/l10n/app_ja.arb b/app/lib/l10n/app_ja.arb index c757b9e..75f064a 100644 --- a/app/lib/l10n/app_ja.arb +++ b/app/lib/l10n/app_ja.arb @@ -37,10 +37,10 @@ "notificationTargetUnavailable": "この通知のエージェントは利用できなくなりました。", "hostPairNotifications": "通知用のペアリングコードを作成", "hostPairingCodeTitle": "通知 plugin をペアリング", - "hostPairingCodeIntro": "Herdr host で、次のコマンドをコピーして実行してください。/path/to/drover-notify は plugin のパスに置き換えます。setup が表示した prompt にペアリングコードを貼り付けてください。", - "hostPairingLinkCommandLabel": "1. plugin を link(初回のみ)", - "hostPairingSetupCommandLabel": "2. setup を実行", - "hostPairingCodeLabel": "3. ペアリングコード", + "hostPairingCodeIntro": "この host に通知 plugin が見つかりませんでした。Herdr host で次のコマンドを実行してインストールし、もう一度「通知用のペアリングコードを作成」をタップしてください。Drover が検出して自動でペアリングします。", + "hostPairingInstallCommandLabel": "plugin をインストール", + "hostPairingManualNote": "手動でペアリングする場合は、plugin の setup スクリプトが下のコードと URL を尋ねます。", + "hostPairingCodeLabel": "ペアリングコード", "hostPairingUrlLabel": "完了 URL", "hostPairAutoDetectedTitle": "通知 plugin を検出しました", "hostPairAutoDetectedBody": "この host には drover.notify plugin がすでに link されています。プッシュ通知のペアリングを自動設定しますか?", diff --git a/app/lib/l10n/app_localizations.dart b/app/lib/l10n/app_localizations.dart index 655864b..f551046 100644 --- a/app/lib/l10n/app_localizations.dart +++ b/app/lib/l10n/app_localizations.dart @@ -323,25 +323,25 @@ abstract class AppLocalizations { /// No description provided for @hostPairingCodeIntro. /// /// In en, this message translates to: - /// **'On the Herdr host, copy and run these commands. Replace /path/to/drover-notify with the plugin path. When setup asks, paste the pairing code.'** + /// **'Drover couldn\'t find the notification plugin on this host. Run this on the Herdr host to install it, then tap Create notification pairing code again — Drover will find it and pair for you.'** String get hostPairingCodeIntro; - /// No description provided for @hostPairingLinkCommandLabel. + /// No description provided for @hostPairingInstallCommandLabel. /// /// In en, this message translates to: - /// **'1. Link the plugin (first time only)'** - String get hostPairingLinkCommandLabel; + /// **'Install the plugin'** + String get hostPairingInstallCommandLabel; - /// No description provided for @hostPairingSetupCommandLabel. + /// No description provided for @hostPairingManualNote. /// /// In en, this message translates to: - /// **'2. Run setup'** - String get hostPairingSetupCommandLabel; + /// **'Pairing by hand instead? The plugin\'s own setup script asks for the code and the URL below.'** + String get hostPairingManualNote; /// No description provided for @hostPairingCodeLabel. /// /// In en, this message translates to: - /// **'3. Pairing code'** + /// **'Pairing code'** String get hostPairingCodeLabel; /// No description provided for @hostPairingUrlLabel. diff --git a/app/lib/l10n/app_localizations_en.dart b/app/lib/l10n/app_localizations_en.dart index c94849d..5749236 100644 --- a/app/lib/l10n/app_localizations_en.dart +++ b/app/lib/l10n/app_localizations_en.dart @@ -137,17 +137,17 @@ class AppLocalizationsEn extends AppLocalizations { @override String get hostPairingCodeIntro => - 'On the Herdr host, copy and run these commands. Replace /path/to/drover-notify with the plugin path. When setup asks, paste the pairing code.'; + 'Drover couldn\'t find the notification plugin on this host. Run this on the Herdr host to install it, then tap Create notification pairing code again — Drover will find it and pair for you.'; @override - String get hostPairingLinkCommandLabel => - '1. Link the plugin (first time only)'; + String get hostPairingInstallCommandLabel => 'Install the plugin'; @override - String get hostPairingSetupCommandLabel => '2. Run setup'; + String get hostPairingManualNote => + 'Pairing by hand instead? The plugin\'s own setup script asks for the code and the URL below.'; @override - String get hostPairingCodeLabel => '3. Pairing code'; + String get hostPairingCodeLabel => 'Pairing code'; @override String get hostPairingUrlLabel => 'Completion URL'; diff --git a/app/lib/l10n/app_localizations_ja.dart b/app/lib/l10n/app_localizations_ja.dart index 2d32daa..8d0ed2e 100644 --- a/app/lib/l10n/app_localizations_ja.dart +++ b/app/lib/l10n/app_localizations_ja.dart @@ -127,16 +127,17 @@ class AppLocalizationsJa extends AppLocalizations { @override String get hostPairingCodeIntro => - 'Herdr host で、次のコマンドをコピーして実行してください。/path/to/drover-notify は plugin のパスに置き換えます。setup が表示した prompt にペアリングコードを貼り付けてください。'; + 'この host に通知 plugin が見つかりませんでした。Herdr host で次のコマンドを実行してインストールし、もう一度「通知用のペアリングコードを作成」をタップしてください。Drover が検出して自動でペアリングします。'; @override - String get hostPairingLinkCommandLabel => '1. plugin を link(初回のみ)'; + String get hostPairingInstallCommandLabel => 'plugin をインストール'; @override - String get hostPairingSetupCommandLabel => '2. setup を実行'; + String get hostPairingManualNote => + '手動でペアリングする場合は、plugin の setup スクリプトが下のコードと URL を尋ねます。'; @override - String get hostPairingCodeLabel => '3. ペアリングコード'; + String get hostPairingCodeLabel => 'ペアリングコード'; @override String get hostPairingUrlLabel => '完了 URL'; diff --git a/app/lib/src/screens/host_setup_screen.dart b/app/lib/src/screens/host_setup_screen.dart index 20248ec..2471ad6 100644 --- a/app/lib/src/screens/host_setup_screen.dart +++ b/app/lib/src/screens/host_setup_screen.dart @@ -241,12 +241,13 @@ class _HostSetupScreenState extends State { context: context, builder: (context) { final l10n = AppLocalizations.of(context)!; - final pluginPath = '/path/to/drover-notify'; final herdrBin = _shellCommandPath(config.herdrBin); - final linkCommand = '$herdrBin plugin link $pluginPath'; - final setupCommand = - 'node $pluginPath/bin/setup.mjs --completion-url ' - '${_shellQuote(pairing.completionUrl)} --herdr-bin $herdrBin'; + // `plugin install` clones from GitHub, so there is no checkout path for + // the user to substitute, and the install location is herdr's to + // choose — which is why no setup command is offered here. Once the + // plugin is installed, [_detectPlugin] finds it and pairing runs + // automatically; the code and URL below are only for pairing by hand. + final installCommand = '$herdrBin plugin install keinstn/drover-notify'; return AlertDialog( title: Text(l10n.hostPairingCodeTitle), content: SingleChildScrollView( @@ -257,14 +258,12 @@ class _HostSetupScreenState extends State { Text(l10n.hostPairingCodeIntro), const SizedBox(height: 16), _CopyableValue( - label: l10n.hostPairingLinkCommandLabel, - value: linkCommand, + key: const ValueKey('pairing_install_command'), + label: l10n.hostPairingInstallCommandLabel, + value: installCommand, ), const SizedBox(height: 16), - _CopyableValue( - label: l10n.hostPairingSetupCommandLabel, - value: setupCommand, - ), + Text(l10n.hostPairingManualNote), const SizedBox(height: 16), _CopyableValue( label: l10n.hostPairingCodeLabel, @@ -516,7 +515,7 @@ String _shellCommandPath(String value) => value.startsWith('~/') String _shellQuote(String value) => "'${value.replaceAll("'", "'\"'\"'")}'"; class _CopyableValue extends StatelessWidget { - const _CopyableValue({required this.label, required this.value}); + const _CopyableValue({super.key, required this.label, required this.value}); final String label; final String value; diff --git a/app/test/screens/host_setup_screen_test.dart b/app/test/screens/host_setup_screen_test.dart index 35e2cd9..55401b4 100644 --- a/app/test/screens/host_setup_screen_test.dart +++ b/app/test/screens/host_setup_screen_test.dart @@ -328,6 +328,65 @@ void main() { await tester.pumpWidget(const SizedBox()); }); + testWidgets( + 'the manual pairing dialog offers `plugin install` with no path to edit', + (tester) async { + tester.view.physicalSize = const Size(800, 1600); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: HostSetupScreen( + initial: const HostConfig( + host: 'example.com', + user: 'dev', + herdrBin: '~/.local/bin/herdr', + privateKeyPem: + '-----BEGIN OPENSSH PRIVATE KEY-----\n' + 'abc\n' + '-----END OPENSSH PRIVATE KEY-----', + ), + onSubmit: (_) async {}, + onCreatePairingCode: (_) async => _samplePairing, + ), + ), + ); + + await tester.tap( + find.widgetWithText(OutlinedButton, 'Create notification pairing code'), + ); + await tester.pumpAndSettle(); + + // The one-liner installs from GitHub, so nothing in it is a placeholder + // the user has to replace by hand. + expect( + find.descendant( + of: find.byKey(const ValueKey('pairing_install_command')), + matching: find.textContaining( + 'plugin install keinstn/drover-notify', + ), + ), + findsOneWidget, + ); + + // Nothing anywhere in the dialog asks the user to substitute a path, and + // the fabricated `node /bin/setup.mjs` step is gone with it. + expect(find.textContaining('/path/to/'), findsNothing); + expect(find.textContaining('plugin link'), findsNothing); + expect(find.textContaining('setup.mjs'), findsNothing); + + // The pairing code and completion URL stay, for pairing by hand. + expect(find.text(_samplePairing.code), findsOneWidget); + expect(find.text(_samplePairing.completionUrl), findsOneWidget); + + await tester.pumpWidget(const SizedBox()); + }, + ); + testWidgets('detected plugin: confirming auto-pairs and shows success', ( tester, ) async {