From 051f8d4e773a65db3ee21cdd70e10783b17e150f Mon Sep 17 00:00:00 2001 From: Dominik Simonik Date: Fri, 13 Nov 2026 10:38:24 +0100 Subject: [PATCH] feat(translate): tell hooks which plugins vgv_ai installed --- ADDING_A_HARNESS.md | 4 + .../lib/src/translate/hook_scripts.dart | 18 ++++ .../translators/antigravity_compat.dart | 7 +- .../translate/translators/codex_compat.dart | 7 +- .../translate/translators/copilot_compat.dart | 7 +- .../installed_plugins_preamble_test.dart | 99 +++++++++++++++++++ 6 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 packages/vgv_ai_cli/test/src/translate/installed_plugins_preamble_test.dart diff --git a/ADDING_A_HARNESS.md b/ADDING_A_HARNESS.md index 5ffa8077..c0972811 100644 --- a/ADDING_A_HARNESS.md +++ b/ADDING_A_HARNESS.md @@ -277,6 +277,10 @@ In the preamble, `__ROOT__`, `__AGENT__` and `__COMPAT__` are replaced with quot plugin root, the agent the hook is scoped to — or an empty string when hooks of more than one agent scope run the script — and the compat script's path. +Before the compat script is sourced, set `VGV_AI_HARNESS=` and add +`$installedPluginsPreamble`. It tells a hook that recommends plugins which ones vgv_ai installed +and how to install another, as `recommend-plugins.sh` in Wingspan reads them. + **The compat script** goes in `_compat.dart` beside the translator. It reads what the harness sends on stdin, hands the script the payload Claude Code would have sent, and respells the answer the way the harness reads it. [`copilot_compat.dart`][copilot_compat] is the one to copy diff --git a/packages/vgv_ai_cli/lib/src/translate/hook_scripts.dart b/packages/vgv_ai_cli/lib/src/translate/hook_scripts.dart index 08b01230..a3910567 100644 --- a/packages/vgv_ai_cli/lib/src/translate/hook_scripts.dart +++ b/packages/vgv_ai_cli/lib/src/translate/hook_scripts.dart @@ -8,6 +8,24 @@ import 'package:vgv_ai_cli/src/translate/copy_source.dart'; import 'package:vgv_ai_cli/src/translate/model/model.dart'; import 'package:vgv_ai_plugins/vgv_ai_plugins.dart'; +/// Goes in every harness's preamble, below a line that sets `VGV_AI_HARNESS` +/// to the harness's name. It exports, for a hook that recommends plugins, +/// the VGV plugins vgv_ai installed for that harness in `VGV_AI_PLUGINS`, +/// separated by spaces, and the command that installs another in +/// `VGV_AI_INSTALL_COMMAND`. +/// +/// The plugins are read from the manifest each run, since a later install +/// adds plugins without rewriting the scripts already copied. A value the +/// script inherits is kept, so the compat script's rerun reads it once. +const installedPluginsPreamble = r''' +export VGV_AI_INSTALL_COMMAND='vgv_ai install --plugin' +[ -n "${VGV_AI_PLUGINS+x}" ] || export VGV_AI_PLUGINS="$( + jq -r --arg harness "$VGV_AI_HARNESS" \ + '.plugins[$harness] // {} | keys | join(" ")' \ + "$HOME/.vgv_ai/manifest.json" 2> /dev/null +)" +'''; + /// {@template hook_scripts} /// A harness's copy of each plugin's `hooks` folder under the hooks root /// and the plugin's name, with the harness's [preamble] below the shebang diff --git a/packages/vgv_ai_cli/lib/src/translate/translators/antigravity_compat.dart b/packages/vgv_ai_cli/lib/src/translate/translators/antigravity_compat.dart index c4406827..6fc1f8cd 100644 --- a/packages/vgv_ai_cli/lib/src/translate/translators/antigravity_compat.dart +++ b/packages/vgv_ai_cli/lib/src/translate/translators/antigravity_compat.dart @@ -1,11 +1,16 @@ +import 'package:vgv_ai_cli/src/translate/translate.dart'; + /// Goes below the shebang of every script a hook runs, in Antigravity's /// copy of the plugin's hooks folder. `__ROOT__` and `__COMPAT__` are /// replaced with quoted shell words: the plugin root and the path of /// [antigravityCompatScript]. -const antigravityPreamble = ''' +const antigravityPreamble = + ''' # Added by vgv_ai install so this Claude Code hook script runs under # Antigravity CLI. export CLAUDE_PLUGIN_ROOT=__ROOT__ +VGV_AI_HARNESS=antigravity +$installedPluginsPreamble . __COMPAT__ '''; diff --git a/packages/vgv_ai_cli/lib/src/translate/translators/codex_compat.dart b/packages/vgv_ai_cli/lib/src/translate/translators/codex_compat.dart index ff16fca0..a2226509 100644 --- a/packages/vgv_ai_cli/lib/src/translate/translators/codex_compat.dart +++ b/packages/vgv_ai_cli/lib/src/translate/translators/codex_compat.dart @@ -1,10 +1,15 @@ +import 'package:vgv_ai_cli/src/translate/translate.dart'; + /// Goes below the shebang of every script a hook runs, in Codex's copy of /// the plugin's hooks folder. `__ROOT__`, `__AGENT__` and `__COMPAT__` are /// replaced with quoted shell words: the plugin root, the agent the hook is /// scoped to or an empty string, and the path of [codexCompatScript]. -const codexPreamble = ''' +const codexPreamble = + ''' # Added by vgv_ai install so this Claude Code hook script runs under Codex. export CLAUDE_PLUGIN_ROOT=__ROOT__ +VGV_AI_HARNESS=codex +$installedPluginsPreamble VGV_AGENT=__AGENT__ . __COMPAT__ '''; diff --git a/packages/vgv_ai_cli/lib/src/translate/translators/copilot_compat.dart b/packages/vgv_ai_cli/lib/src/translate/translators/copilot_compat.dart index 97406075..8d8d16a4 100644 --- a/packages/vgv_ai_cli/lib/src/translate/translators/copilot_compat.dart +++ b/packages/vgv_ai_cli/lib/src/translate/translators/copilot_compat.dart @@ -1,10 +1,15 @@ +import 'package:vgv_ai_cli/src/translate/translate.dart'; + /// Goes below the shebang of every script a hook runs, in Copilot's copy of /// the plugin's hooks folder. `__ROOT__` and `__COMPAT__` are replaced with /// quoted shell words: the plugin root and the path of /// [copilotCompatScript]. -const copilotPreamble = ''' +const copilotPreamble = + ''' # Added by vgv_ai install so this Claude Code hook script runs under Copilot. export CLAUDE_PLUGIN_ROOT=__ROOT__ +VGV_AI_HARNESS=copilot +$installedPluginsPreamble . __COMPAT__ '''; diff --git a/packages/vgv_ai_cli/test/src/translate/installed_plugins_preamble_test.dart b/packages/vgv_ai_cli/test/src/translate/installed_plugins_preamble_test.dart new file mode 100644 index 00000000..472ef802 --- /dev/null +++ b/packages/vgv_ai_cli/test/src/translate/installed_plugins_preamble_test.dart @@ -0,0 +1,99 @@ +import 'dart:io'; + +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:test/test.dart'; +import 'package:vgv_ai_cli/src/model/model.dart'; +import 'package:vgv_ai_cli/src/translate/translate.dart'; + +/// Runs [installedPluginsPreamble] for real, through bash and jq. Skipped +/// where bash or jq is missing. +void main() { + final bash = Process.runSync('bash', ['-c', 'command -v jq']).exitCode == 0; + + group('installedPluginsPreamble', () { + late Directory home; + late File script; + + setUp(() { + home = const LocalFileSystem().systemTempDirectory.createTempSync(); + addTearDown(() => home.deleteSync(recursive: true)); + script = home.childFile('hook.sh') + ..writeAsStringSync( + '#!/bin/bash\n' + 'VGV_AI_HARNESS=codex\n' + '$installedPluginsPreamble' + r'printf "%s|%s" "$VGV_AI_PLUGINS" "$VGV_AI_INSTALL_COMMAND"', + ); + }); + + /// Writes the manifest vgv_ai keeps under [home]. + void manifest(String json) { + home.childDirectory('.vgv_ai').childFile('manifest.json') + ..createSync(recursive: true) + ..writeAsStringSync(json); + } + + /// What the script saw, with [env] on top of a [home] of its own. + String run([Map env = const {}]) { + final result = Process.runSync( + 'bash', + [script.path], + environment: {'HOME': home.path, ...env}, + ); + return result.stdout as String; + } + + test( + 'export the plugins installed for the harness and the install command', + () { + manifest(''' +{ + "version": 3, + "plugins": { + "codex": {"vgv-wingspan": "0.1.0", "vgv-ai-flutter-plugin": "0.1.0"}, + "copilot": {"vgv-ffca-plugin": "0.1.0"} + } +}'''); + + expect( + run(), + 'vgv-ai-flutter-plugin vgv-wingspan|vgv_ai install --plugin', + ); + }, + skip: !bash, + ); + + test('export no plugins when the harness has none', () { + manifest('{"version": 3, "plugins": {"copilot": {"vgv-wingspan": "1"}}}'); + + expect(run(), '|vgv_ai install --plugin'); + }, skip: !bash); + + test('export no plugins when there is no manifest', () { + expect(run(), '|vgv_ai install --plugin'); + }, skip: !bash); + + test('keep the plugins the script inherits', () { + manifest('{"version": 3, "plugins": {"codex": {"vgv-wingspan": "1"}}}'); + + expect( + run({'VGV_AI_PLUGINS': 'inherited'}), + 'inherited|vgv_ai install --plugin', + ); + }, skip: !bash); + }); + + test('every preamble exports the installed plugins for its harness', () { + for (final (harness, preamble) in [ + (Harness.codex, codexPreamble), + (Harness.copilot, copilotPreamble), + (Harness.antigravity, antigravityPreamble), + ]) { + expect( + preamble, + contains('VGV_AI_HARNESS=${harness.name}\n$installedPluginsPreamble'), + ); + } + }); +}