diff --git a/src/local-mounts/README.md b/src/local-mounts/README.md index c38a453..766f9c3 100644 --- a/src/local-mounts/README.md +++ b/src/local-mounts/README.md @@ -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 @@ -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 @@ -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 diff --git a/src/local-mounts/devcontainer-feature.json b/src/local-mounts/devcontainer-feature.json index 88d6d20..b9805b3 100644 --- a/src/local-mounts/devcontainer-feature.json +++ b/src/local-mounts/devcontainer-feature.json @@ -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", diff --git a/src/local-mounts/install.sh b/src/local-mounts/install.sh index 68e697b..c011e04 100644 --- a/src/local-mounts/install.sh +++ b/src/local-mounts/install.sh @@ -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 @@ -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 @@ -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 diff --git a/src/peon-ping/README.md b/src/peon-ping/README.md index 302424f..ad2e481 100644 --- a/src/peon-ping/README.md +++ b/src/peon-ping/README.md @@ -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 diff --git a/src/peon-ping/devcontainer-feature.json b/src/peon-ping/devcontainer-feature.json index e07d3bf..b4a380d 100644 --- a/src/peon-ping/devcontainer-feature.json +++ b/src/peon-ping/devcontainer-feature.json @@ -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", diff --git a/src/peon-ping/install.sh b/src/peon-ping/install.sh index 8953950..6f2272a 100644 --- a/src/peon-ping/install.sh +++ b/src/peon-ping/install.sh @@ -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 @@ -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 @@ -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 @@ -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": {}} @@ -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 } @@ -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] @@ -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" } ] } }