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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
{
"name": "tool-routing",
"source": "./plugins/tool-routing",
"version": "1.1.1"
"version": "1.2.0"
},
{
"name": "stay-on-target",
Expand Down
28 changes: 18 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,20 @@ When installed, plugins are copied to cache. Environment:
### Test tool-routing routes

```bash
# From repo root
cd plugins/tool-routing
CLAUDE_PLUGIN_ROOT="$PWD" CLAUDE_PLUGINS_DIR="../" uv run tool-routing test
# From repo root - CLAUDE_PROJECT_ROOT must match where plugin is scoped
CLAUDE_PROJECT_ROOT="$PWD" uv run --directory plugins/tool-routing tool-routing test
```

### Verify cross-plugin route discovery

```bash
CLAUDE_PLUGIN_ROOT="$PWD" CLAUDE_PLUGINS_DIR="../" uv run tool-routing list
# Should show routes from multiple plugins
# From repo root
CLAUDE_PROJECT_ROOT="$PWD" uv run --directory plugins/tool-routing tool-routing list
# Shows routes from enabled plugins with routes.json manifests
```

**Important:** The tool-routing plugin uses manifest-driven discovery via `claude plugin list --json`. Local-scoped plugins are only discovered when `CLAUDE_PROJECT_ROOT` (or cwd) **exactly matches** the plugin's `projectPath`.

## Common Issues

### Plugin hooks not running after code changes
Expand All @@ -64,14 +66,19 @@ The marketplace uses `"source": "directory"` but still **copies** to cache at in

### Routes only discovered from one source

The `derive_plugins_dir()` function may not be finding sibling plugins.
Discovery uses `claude plugin list --json` and filters by enabled status and project path.

**Check:**
```bash
# Verify all plugins are in cache
ls ~/.claude/plugins/cache/technicalpickles-marketplace/
# See which plugins are enabled and their project paths
claude plugin list --json | jq '.[] | select(.enabled) | {id, scope, projectPath}'

# Verify routes.json exists in cache
ls ~/.claude/plugins/cache/pickled-claude-plugins/*/latest/.claude-plugin/routes.json
```

**Common cause:** Running from a subdirectory (e.g., `plugins/tool-routing/`) when plugins are scoped to the repo root. Local-scoped plugins require exact `projectPath` match.

### `installed_plugins.json` points to non-existent path

This can happen with directory-source marketplaces.
Expand All @@ -88,8 +95,9 @@ rm -rf ~/.claude/plugins/cache/technicalpickles-marketplace/{plugin}/
| Variable | Set By | Purpose |
|----------|--------|---------|
| `CLAUDE_PLUGIN_ROOT` | Claude Code (plugin hooks only) | Plugin's cache directory |
| `CLAUDE_PLUGINS_DIR` | tool-routing (derived) | Parent directory for cross-plugin discovery |
| `CLAUDE_PROJECT_DIR` | Claude Code | Current project directory |
| `CLAUDE_PROJECT_ROOT` | tool-routing CLI | Project root for filtering local-scoped plugins |
| `TOOL_ROUTING_ROUTES` | Manual (testing) | Explicit route file paths, bypasses discovery |
| `TOOL_ROUTING_DEBUG` | Manual | Enable debug output for route matching |

**Note:** `CLAUDE_PLUGIN_ROOT` is NOT set for global hooks in `~/.claude/settings.json`.

Expand Down
156 changes: 47 additions & 109 deletions bin/test-claude
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
#!/bin/bash
# test-claude - Run Claude Code with isolated test configuration
#
# This wrapper sets up an isolated Claude config directory with only
# This wrapper sets up an isolated Claude config directory with all
# local plugins installed, useful for testing plugin hooks.
#
# Usage:
# ./bin/test-claude # Setup (if needed), install plugins, start claude
# ./bin/test-claude --clean # Remove test config and start fresh
# ./bin/test-claude -p "prompt" # Pass args to claude

set -e

Expand All @@ -25,36 +30,29 @@ usage() {
Usage: test-claude [OPTIONS] [CLAUDE_ARGS...]

Run Claude Code with an isolated test configuration.
Automatically sets up config and installs all local plugins on first run.

Options:
--setup Initialize/reset the test environment
--install Install tool-routing plugin to test config
--status Show test environment status
--clean Remove test config and start fresh
--reset Reset settings.json (re-copies from global config)
--clean Remove test config (next run will reinitialize)
--help Show this help message

Examples:
test-claude --setup # First-time setup
test-claude --install # Install tool-routing plugin
test-claude # Run claude with test config
test-claude # Setup + install + run claude
test-claude -p "test prompt" # Run with print mode
test-claude --clean # Remove test config
EOF
}

setup_marketplace() {
log "Setting up local marketplace..."
setup_config() {
log "Setting up test config..."

# Create directories
mkdir -p "$TEST_CONFIG/plugins/cache"
mkdir -p "$TEST_CONFIG/plugins/marketplaces"

# Symlink our repo as a marketplace
if [ ! -L "$TEST_CONFIG/plugins/marketplaces/local-test" ]; then
ln -sf "$REPO_ROOT" "$TEST_CONFIG/plugins/marketplaces/local-test"
log "Created marketplace symlink: local-test -> $REPO_ROOT"
fi

ln -sf "$REPO_ROOT" "$TEST_CONFIG/plugins/marketplaces/local-test"

# Create known_marketplaces.json
cat > "$TEST_CONFIG/plugins/known_marketplaces.json" << EOF
{
Expand All @@ -68,19 +66,17 @@ setup_marketplace() {
}
}
EOF
log "Created known_marketplaces.json"


# Create settings.json with bedrock settings from global config
if [ ! -f "$TEST_CONFIG/settings.json" ]; then
GLOBAL_SETTINGS="$HOME/.claude/settings.json"
GLOBAL_SETTINGS="$HOME/.claude/settings.json"

if [ -f "$GLOBAL_SETTINGS" ]; then
# Extract bedrock-related settings from global config
ENV_SETTINGS=$(jq '.env // {}' "$GLOBAL_SETTINGS")
AWS_AUTH=$(jq -r '.awsAuthRefresh // empty' "$GLOBAL_SETTINGS")
if [ -f "$GLOBAL_SETTINGS" ]; then
# Extract bedrock-related settings from global config
ENV_SETTINGS=$(jq '.env // {}' "$GLOBAL_SETTINGS")
AWS_AUTH=$(jq -r '.awsAuthRefresh // empty' "$GLOBAL_SETTINGS")

# Build settings with bedrock config
cat > "$TEST_CONFIG/settings.json" << EOF
# Build settings with bedrock config
cat > "$TEST_CONFIG/settings.json" << EOF
{
"env": $ENV_SETTINGS,
$([ -n "$AWS_AUTH" ] && echo "\"awsAuthRefresh\": \"$AWS_AUTH\",")
Expand All @@ -96,10 +92,9 @@ EOF
"hooks": {}
}
EOF
log "Created settings.json with bedrock config from global settings"
else
# Fallback to minimal settings
cat > "$TEST_CONFIG/settings.json" << EOF
else
# Fallback to minimal settings
cat > "$TEST_CONFIG/settings.json" << EOF
{
"permissions": {
"allow": [
Expand All @@ -113,109 +108,52 @@ EOF
"hooks": {}
}
EOF
log "Created minimal settings.json (no global config found)"
fi
fi

# Create empty installed_plugins.json
if [ ! -f "$TEST_CONFIG/plugins/installed_plugins.json" ]; then
echo '{"version": 2, "plugins": {}}' > "$TEST_CONFIG/plugins/installed_plugins.json"
log "Created installed_plugins.json"
fi

log "Setup complete!"
}

install_plugin() {
local plugin="${1:-tool-routing}"
log "Installing $plugin from local-test marketplace..."

# Run claude plugin install with our test config
CLAUDE_CONFIG_DIR="$TEST_CONFIG" claude plugin install "$plugin@local-test"

log "Plugin installed. Restart claude to load hooks."
# Create empty installed_plugins.json
echo '{"version": 2, "plugins": {}}' > "$TEST_CONFIG/plugins/installed_plugins.json"
}

show_status() {
log "Test environment status:"
echo ""

if [ -d "$TEST_CONFIG" ]; then
echo "Config directory: $TEST_CONFIG"
echo ""

echo "Marketplaces:"
if [ -f "$TEST_CONFIG/plugins/known_marketplaces.json" ]; then
cat "$TEST_CONFIG/plugins/known_marketplaces.json" | jq -r 'keys[]' 2>/dev/null || echo " (error reading)"
else
echo " (none)"
fi
echo ""

echo "Installed plugins:"
if [ -f "$TEST_CONFIG/plugins/installed_plugins.json" ]; then
cat "$TEST_CONFIG/plugins/installed_plugins.json" | jq -r '.plugins | keys[]' 2>/dev/null || echo " (none or error)"
else
echo " (none)"
install_all_plugins() {
log "Installing plugins from local-test marketplace..."

# Find all plugins with .claude-plugin directories
for plugin_dir in "$REPO_ROOT"/plugins/*/; do
if [ -d "$plugin_dir/.claude-plugin" ]; then
plugin_name=$(basename "$plugin_dir")
log " Installing $plugin_name..."
CLAUDE_CONFIG_DIR="$TEST_CONFIG" claude plugin install "$plugin_name@local-test" 2>/dev/null || {
warn " Failed to install $plugin_name (may not be in marketplace.json)"
}
fi
echo ""

echo "Hooks in settings.json:"
if [ -f "$TEST_CONFIG/settings.json" ]; then
cat "$TEST_CONFIG/settings.json" | jq '.hooks' 2>/dev/null || echo " (error reading)"
fi
else
warn "Test config not initialized. Run: test-claude --setup"
fi
done
}

clean_config() {
warn "Removing test configuration..."
rm -rf "$TEST_CONFIG"
log "Clean complete. Run --setup to reinitialize."
}

reset_settings() {
log "Resetting settings.json..."
rm -f "$TEST_CONFIG/settings.json"
setup_marketplace
log "Clean complete. Next run will reinitialize."
}

# Parse arguments
case "${1:-}" in
--setup)
setup_marketplace
exit 0
;;
--install)
shift
install_plugin "$@"
exit 0
;;
--status)
show_status
exit 0
;;
--clean)
clean_config
exit 0
;;
--reset)
reset_settings
exit 0
;;
--help|-h)
usage
exit 0
;;
esac

# Verify setup exists
# Setup if needed
if [ ! -d "$TEST_CONFIG" ]; then
error "Test config not initialized. Run: test-claude --setup"
exit 1
setup_config
install_all_plugins
log "Setup complete!"
fi

# Run claude with test config
log "Running claude with CLAUDE_CONFIG_DIR=$TEST_CONFIG"
log "Starting claude with test config..."
exec env CLAUDE_CONFIG_DIR="$TEST_CONFIG" claude "$@"
Loading