Skip to content
Draft
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
4 changes: 4 additions & 0 deletions ADDING_A_HARNESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<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 `<harness>_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
Expand Down
18 changes: 18 additions & 0 deletions packages/vgv_ai_cli/lib/src/translate/hook_scripts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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__
''';

Expand Down
Original file line number Diff line number Diff line change
@@ -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__
''';

Expand Down
Original file line number Diff line number Diff line change
@@ -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__
''';

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> 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'),
);
}
});
}
Loading