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
6 changes: 3 additions & 3 deletions src/local-mounts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ For **optimal reliability** (especially across container rebuilds and reconnecti
```bash
# Stable SSH agent socket (optional, recommended for devcontainers)
export SSH_AUTH_SOCK="$HOME/.ssh/agent.sock"
if [[ ! -S "$SSH_AUTH_SOCK" ]]; then
if ! ssh-add -l &>/dev/null; then
rm -f "$SSH_AUTH_SOCK"
eval "$(ssh-agent -a "$SSH_AUTH_SOCK")" >/dev/null
ssh-add 2>/dev/null
Expand All @@ -72,7 +72,7 @@ fi
```bash
# Stable SSH agent socket (optional, recommended for devcontainers)
export SSH_AUTH_SOCK="$HOME/.ssh/agent.sock"
if [ ! -S "$SSH_AUTH_SOCK" ]; then
if ! ssh-add -l >/dev/null 2>&1; then
rm -f "$SSH_AUTH_SOCK"
eval "$(ssh-agent -a "$SSH_AUTH_SOCK")" > /dev/null
ssh-add 2>/dev/null
Expand All @@ -83,7 +83,7 @@ fi

```bash
export SSH_AUTH_SOCK="$HOME/.ssh/agent.sock"
if [[ ! -S "$SSH_AUTH_SOCK" ]]; then
if ! ssh-add -l &>/dev/null; then
rm -f "$SSH_AUTH_SOCK"
eval "$(ssh-agent -a "$SSH_AUTH_SOCK")" >/dev/null
ssh-add --apple-use-keychain 2>/dev/null
Expand Down
2 changes: 1 addition & 1 deletion src/local-mounts/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "local-mounts",
"version": "1.0.7",
"version": "1.0.8",
"name": "Local Development Files Mount",
"description": "Mounts local Git, SSH, GPG, and npm configuration files into the devcontainer with support for custom usernames. Uses runtime SSH agent detection with fallback to VS Code native forwarding.",
"documentationURL": "https://github.com/helpers4/devcontainer/tree/main/features/local-mounts",
Expand Down
48 changes: 32 additions & 16 deletions src/local-mounts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ _sync_dir_from_mount() {

if [ -d "${source_dir}" ]; then
mkdir -p "${target_dir}" 2>/dev/null || true
cp -a "${source_dir}/." "${target_dir}/" 2>/dev/null || true
if ! cp -a "${source_dir}/." "${target_dir}/" 2>/dev/null; then
echo "⚠️ cp -a failed for ${config_name}, falling back to file-by-file copy"
find "${source_dir}" -maxdepth 1 -type f -exec cp -f {} "${target_dir}/" \;
fi
echo "✅ ${config_name} synchronized to ${target_dir}"
elif [ ! -d "${target_dir}" ]; then
mkdir -p "${target_dir}" 2>/dev/null || true
Expand Down Expand Up @@ -87,18 +90,22 @@ _sync_file_from_mount "${SOURCE_HOME}/.npmrc" "${TARGET_HOME}/.npmrc" ".npmrc"
# Explicitly ensure SSH keys/config and GPG private keys are synced
# ============================================================================

# Ensure SSH keys and config are synced
# Ensure ALL SSH files are synced (not just id_* keys)
if [ -d "${SOURCE_HOME}/.ssh" ]; then
for f in "${SOURCE_HOME}/.ssh/id_"*; do
[ -f "$f" ] && cp -f "$f" "${TARGET_HOME}/.ssh/" 2>/dev/null || true
done
[ -f "${SOURCE_HOME}/.ssh/config" ] && \
cp -f "${SOURCE_HOME}/.ssh/config" "${TARGET_HOME}/.ssh/" 2>/dev/null || true
chmod 700 "${TARGET_HOME}/.ssh" 2>/dev/null || true
find "${TARGET_HOME}/.ssh" -name "id_*" ! -name "*.pub" -exec chmod 600 {} \; 2>/dev/null || true
find "${TARGET_HOME}/.ssh" -name "*.pub" -exec chmod 644 {} \; 2>/dev/null || true
[ -f "${TARGET_HOME}/.ssh/config" ] && chmod 600 "${TARGET_HOME}/.ssh/config" 2>/dev/null || true
echo "✅ SSH keys and config synchronized"
find "${SOURCE_HOME}/.ssh" -maxdepth 1 -type f -exec cp -f {} "${TARGET_HOME}/.ssh/" \;
# Fix permissions
chmod 700 "${TARGET_HOME}/.ssh"
find "${TARGET_HOME}/.ssh" -name "id_*" ! -name "*.pub" -exec chmod 600 {} \;
find "${TARGET_HOME}/.ssh" -name "*.pub" -exec chmod 644 {} \;
[ -f "${TARGET_HOME}/.ssh/config" ] && chmod 600 "${TARGET_HOME}/.ssh/config"
# Verify sync completeness
SOURCE_COUNT=$(find "${SOURCE_HOME}/.ssh" -maxdepth 1 -type f 2>/dev/null | wc -l)
TARGET_COUNT=$(find "${TARGET_HOME}/.ssh" -maxdepth 1 -type f 2>/dev/null | wc -l)
if [ "$SOURCE_COUNT" -gt 0 ] && [ "$TARGET_COUNT" -lt "$SOURCE_COUNT" ]; then
echo "⚠️ SSH sync incomplete: ${TARGET_COUNT}/${SOURCE_COUNT} files copied"
else
echo "✅ SSH files synchronized (${TARGET_COUNT} files)"
fi
fi

# Ensure GPG private keys are synced
Expand Down Expand Up @@ -162,20 +169,29 @@ echo ""

cat > /etc/profile.d/local-mounts-ssh.sh << 'PROFILE_EOF'
# local-mounts: SSH agent socket detection (runtime)
# ssh-add -l exits: 0 = keys loaded, 1 = no keys, 2 = cannot connect
# We accept 0 and 1 (agent alive), reject only 2 (dead/missing agent)
_LOCAL_MOUNTS_SSH_SOCK="/tmp/local-mounts/.ssh/agent.sock"

if [ -S "$_LOCAL_MOUNTS_SSH_SOCK" ]; then
# Stable host socket is mounted and active
_ssh_agent_responds() {
local _rc=0
SSH_AUTH_SOCK="$1" ssh-add -l >/dev/null 2>&1 || _rc=$?
[ "$_rc" -ne 2 ]
}

if [ -S "$_LOCAL_MOUNTS_SSH_SOCK" ] && _ssh_agent_responds "$_LOCAL_MOUNTS_SSH_SOCK"; then
# Stable host socket is mounted and agent responds
export SSH_AUTH_SOCK="$_LOCAL_MOUNTS_SSH_SOCK"
elif [ -n "$SSH_AUTH_SOCK" ] && [ -S "$SSH_AUTH_SOCK" ]; then
elif [ -n "$SSH_AUTH_SOCK" ] && [ -S "$SSH_AUTH_SOCK" ] && _ssh_agent_responds "$SSH_AUTH_SOCK"; then
# VS Code's native SSH agent forwarding is working — keep it
:
elif [ -S "/ssh-agent" ]; then
elif [ -S "/ssh-agent" ] && _ssh_agent_responds "/ssh-agent"; then
# Legacy external mount
export SSH_AUTH_SOCK="/ssh-agent"
fi

unset _LOCAL_MOUNTS_SSH_SOCK
unset -f _ssh_agent_responds
PROFILE_EOF

chmod +x /etc/profile.d/local-mounts-ssh.sh
Expand Down
2 changes: 1 addition & 1 deletion src/peon-ping/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Or add it to your devcontainer.json:
}
```

This creates hooks for `sessionStart`, `userPromptSubmitted`, `postToolUse`, and `errorOccurred` events using the Copilot adapter.
This creates hooks for `SessionStart`, `UserPromptSubmit`, `PostToolUse`, and `Stop` events using the Copilot adapter.

### Cursor

Expand Down
2 changes: 1 addition & 1 deletion src/peon-ping/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "peon-ping",
"version": "1.0.0",
"version": "1.0.1",
"name": "Peon Ping — AI Agent Sound Notifications",
"description": "Installs peon-ping and the Peon Pet VS Code extension for game character voice notifications when your AI coding agent finishes or needs permission. Supports VS Code/Copilot, Cursor, and Codex with audio relay for devcontainers.",
"documentationURL": "https://github.com/helpers4/devcontainer/tree/main/src/peon-ping",
Expand Down
120 changes: 77 additions & 43 deletions src/peon-ping/install.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env bash

# Peon Ping DevContainer Feature
# Copyright (c) 2025 helpers4
# Licensed under LGPL-3.0 - see LICENSE file for details
# This file is part of helpers4.
# Copyright (C) 2025 baxyz
# SPDX-License-Identifier: LGPL-3.0-or-later
#
# Installs peon-ping and configures multi-IDE hooks for AI agent sound notifications

Expand All @@ -14,6 +14,15 @@ NO_RC="${NORC:-"true"}"
IDE_SETUP="${IDESETUP:-"vscode"}"
VOLUME="${VOLUME:-"0.5"}"

# Validate volume is a valid float between 0.0 and 1.0
if ! echo "${VOLUME}" | grep -qE '^[0-9]+(\.[0-9]+)?$'; then
echo "⚠️ Invalid volume '${VOLUME}', falling back to 0.5"
VOLUME="0.5"
elif ! awk "BEGIN { v=${VOLUME}; exit (v >= 0.0 && v <= 1.0) ? 0 : 1 }"; then
echo "⚠️ Volume '${VOLUME}' out of range [0.0–1.0], clamping to 0.5"
VOLUME="0.5"
fi

USERNAME="${USERNAME:-"${_REMOTE_USER:-"automatic"}"}"

if [ "$(id -u)" -ne 0 ]; then
Expand Down Expand Up @@ -113,14 +122,18 @@ PEON_CONFIG="${PEON_CONFIG_DIR}/config.json"
if [ -f "${PEON_CONFIG}" ] && command -v python3 > /dev/null 2>&1; then
echo "🔧 Setting volume to ${VOLUME}..."
python3 << PYEOF
import json
import json, sys

path = "${PEON_CONFIG}"
with open(path) as f:
cfg = json.load(f)
cfg["volume"] = float("${VOLUME}")
with open(path, "w") as f:
json.dump(cfg, f, indent=2)
f.write("\n")
try:
with open(path) as f:
cfg = json.load(f)
cfg["volume"] = max(0.0, min(1.0, float("${VOLUME}")))
with open(path, "w") as f:
json.dump(cfg, f, indent=2)
f.write("\n")
except (json.JSONDecodeError, ValueError, OSError) as e:
print(f"⚠️ Could not update volume in {path}: {e}", file=sys.stderr)
PYEOF
fi

Expand All @@ -146,15 +159,24 @@ merge_hooks_json() {
local new_hooks="$2"

python3 << PYEOF
import json, os
import json, os, sys

target_path = "${target}"
new_hooks = json.loads("""${new_hooks}""")

if os.path.exists(target_path):
with open(target_path) as f:
data = json.load(f)
else:
try:
new_hooks = json.loads("""${new_hooks}""")
except json.JSONDecodeError as e:
print(f"⚠️ Invalid hooks JSON: {e}", file=sys.stderr)
sys.exit(0)

try:
if os.path.exists(target_path):
with open(target_path) as f:
data = json.load(f)
else:
os.makedirs(os.path.dirname(target_path), exist_ok=True)
data = {"version": 1, "hooks": {}}
except (json.JSONDecodeError, OSError) as e:
print(f"⚠️ Could not read {target_path}, creating fresh: {e}", file=sys.stderr)
os.makedirs(os.path.dirname(target_path), exist_ok=True)
data = {"version": 1, "hooks": {}}

Expand All @@ -164,14 +186,16 @@ for event, entries in new_hooks.items():
event_list = existing_hooks.setdefault(event, [])
existing_cmds = [e.get("bash", e.get("command", "")) for e in event_list]
for entry in entries:
cmd = entry.get("bash", entry.get("command", ""))
if not any("peon-ping" in c for c in existing_cmds):
event_list.append(entry)

data["hooks"] = existing_hooks
with open(target_path, "w") as f:
json.dump(data, f, indent=2)
f.write("\n")
try:
with open(target_path, "w") as f:
json.dump(data, f, indent=2)
f.write("\n")
except OSError as e:
print(f"⚠️ Could not write {target_path}: {e}", file=sys.stderr)
PYEOF
}

Expand All @@ -195,24 +219,31 @@ HOOKS_FILE="${HOOKS_DIR}/hooks.json"
mkdir -p "${HOOKS_DIR}"

if [ -f "${HOOKS_FILE}" ]; then
python3 << 'PYEOF'
import json, os
HOOKS_FILE="${HOOKS_FILE}" python3 << 'PYEOF'
import json, os, sys

path = os.environ.get("HOOKS_FILE", ".github/hooks/hooks.json")
if not os.path.exists(path):
exit(0)
sys.exit(0)

with open(path) as f:
data = json.load(f)
try:
with open(path) as f:
data = json.load(f)
except (json.JSONDecodeError, OSError) as e:
print(f"⚠️ Could not read {path}: {e}", file=sys.stderr)
data = {"version": 1, "hooks": {}}

hooks = data.setdefault("hooks", {})
new_entries = {
"sessionStart": [{"type": "command", "bash": "bash ~/.claude/hooks/peon-ping/adapters/copilot.sh sessionStart"}],
"userPromptSubmitted": [{"type": "command", "bash": "bash ~/.claude/hooks/peon-ping/adapters/copilot.sh userPromptSubmitted"}],
"postToolUse": [{"type": "command", "bash": "bash ~/.claude/hooks/peon-ping/adapters/copilot.sh postToolUse"}],
"errorOccurred": [{"type": "command", "bash": "bash ~/.claude/hooks/peon-ping/adapters/copilot.sh errorOccurred"}]
}

# VS Code hook event names (PascalCase, auto-mapped from lowerCamelCase by VS Code).
# See https://code.visualstudio.com/docs/copilot/customization/hooks#_hook-lifecycle-events
# VS Code/Copilot hook event names as written in hooks.json.
"SessionStart": [{"type": "command", "bash": "bash ~/.claude/hooks/peon-ping/adapters/copilot.sh SessionStart"}],
"UserPromptSubmit": [{"type": "command", "bash": "bash ~/.claude/hooks/peon-ping/adapters/copilot.sh UserPromptSubmit"}],
"PostToolUse": [{"type": "command", "bash": "bash ~/.claude/hooks/peon-ping/adapters/copilot.sh PostToolUse"}],
"Stop": [{"type": "command", "bash": "bash ~/.claude/hooks/peon-ping/adapters/copilot.sh Stop"}]
}
"ErrorOccurred": [{"type": "command", "bash": "bash ~/.claude/hooks/peon-ping/adapters/copilot.sh ErrorOccurred"}]
for event, entries in new_entries.items():
event_list = hooks.setdefault(event, [])
existing_cmds = [e.get("bash", e.get("command", "")) for e in event_list]
Expand All @@ -221,26 +252,29 @@ for event, entries in new_entries.items():
event_list.append(entry)

data["hooks"] = hooks
with open(path, "w") as f:
json.dump(data, f, indent=2)
f.write("\n")
try:
with open(path, "w") as f:
json.dump(data, f, indent=2)
f.write("\n")
except OSError as e:
print(f"⚠️ Could not write {path}: {e}", file=sys.stderr)
PYEOF
else
cat > "${HOOKS_FILE}" << 'JSONEOF'
{
"version": 1,
"hooks": {
"sessionStart": [
{ "type": "command", "bash": "bash ~/.claude/hooks/peon-ping/adapters/copilot.sh sessionStart" }
"SessionStart": [
{ "type": "command", "bash": "bash ~/.claude/hooks/peon-ping/adapters/copilot.sh SessionStart" }
],
"userPromptSubmitted": [
{ "type": "command", "bash": "bash ~/.claude/hooks/peon-ping/adapters/copilot.sh userPromptSubmitted" }
"UserPromptSubmit": [
{ "type": "command", "bash": "bash ~/.claude/hooks/peon-ping/adapters/copilot.sh UserPromptSubmit" }
],
"postToolUse": [
{ "type": "command", "bash": "bash ~/.claude/hooks/peon-ping/adapters/copilot.sh postToolUse" }
"PostToolUse": [
{ "type": "command", "bash": "bash ~/.claude/hooks/peon-ping/adapters/copilot.sh PostToolUse" }
],
"errorOccurred": [
{ "type": "command", "bash": "bash ~/.claude/hooks/peon-ping/adapters/copilot.sh errorOccurred" }
"Stop": [
{ "type": "command", "bash": "bash ~/.claude/hooks/peon-ping/adapters/copilot.sh Stop" }
]
}
}
Expand Down
Loading