P0 fixes: - agent register: upsert by agent_id (no duplicate rows) - daemon poll-once: extract _gmail_poll_cycle, run synchronously - garc_core.py: suppress urllib3/googleapiclient DeprecationWarnings P1 fixes: - OAuth: detect RefreshError → delete stale token → re-auth flow - OAuth: scope coverage check before returning valid creds - ingress: add stale-reset subcommand (reset in_progress > N min) - sheets: trim-sheet / clean-all — deleteDimension for empty rows - approval gate: send Gmail notification to GARC_APPROVAL_EMAIL P2 additions: - Google Chat: garc-chat-helper.py + garc send chat subcommands - Service Account: garc auth service-account verify + DWD support - Audit log: Sheets audit tab + garc audit list + bin/garc async hook - garc auth revoke: POST /revoke + delete token file - kg: pagination fix, shell injection fix, garc-kg-query.py - docs: _doc_insert_text / append_doc / garc drive append-doc P3 additions: - Multi-tenant: lib/profile.sh (list/use/add/show/remove/current) bin/garc: auto-load profile config.env and token.json - Google Forms pipeline: garc-forms-helper.py + lib/forms.sh garc forms list/responses/watch - systemd: _daemon_install_service OS-detect → launchd or systemd units - Python version gate (>=3.10) in bin/garc + pyproject.toml - garc doctor command for environment diagnostics Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
215 lines
6.3 KiB
Bash
215 lines
6.3 KiB
Bash
#!/usr/bin/env bash
|
|
# GARC profile.sh — Multi-tenant profile management
|
|
#
|
|
# Profiles live at: ~/.garc/profiles/<name>/
|
|
# config.env — tenant-specific env vars (GARC_DRIVE_FOLDER_ID, GARC_SHEETS_ID, ...)
|
|
# token.json — OAuth token for this tenant
|
|
# credentials.json — OAuth client credentials (optional; falls back to ~/.garc/credentials.json)
|
|
#
|
|
# Usage:
|
|
# garc profile list List all profiles
|
|
# garc profile use <name> Switch active profile (sets GARC_PROFILE)
|
|
# garc profile add <name> Create a new profile directory
|
|
# garc profile show [<name>] Show profile config
|
|
# garc profile remove <name> Delete a profile
|
|
# garc profile current Show current active profile
|
|
|
|
GARC_PROFILES_DIR="${GARC_CONFIG:-${HOME}/.garc}/profiles"
|
|
|
|
garc_profile() {
|
|
local subcommand="${1:-list}"
|
|
shift || true
|
|
|
|
case "${subcommand}" in
|
|
list) _profile_list ;;
|
|
use) _profile_use "$@" ;;
|
|
add) _profile_add "$@" ;;
|
|
show) _profile_show "$@" ;;
|
|
remove) _profile_remove "$@" ;;
|
|
current) _profile_current ;;
|
|
*)
|
|
cat <<EOF
|
|
Usage: garc profile <subcommand>
|
|
|
|
Subcommands:
|
|
list List all configured profiles
|
|
use <name> Print shell command to activate a profile
|
|
add <name> Create a new empty profile directory
|
|
show [<name>] Show the config for a profile (default: current)
|
|
remove <name> Delete a profile (prompts for confirmation)
|
|
current Show the currently active profile name
|
|
|
|
How to use:
|
|
# Add profile-specific config to ~/.garc/profiles/<name>/config.env
|
|
# Then activate with:
|
|
eval "\$(garc profile use <name>)"
|
|
# Or export directly:
|
|
export GARC_PROFILE=<name>
|
|
|
|
Profile config.env example:
|
|
GARC_DRIVE_FOLDER_ID=1xxxxxx
|
|
GARC_SHEETS_ID=1xxxxxx
|
|
GARC_GMAIL_DEFAULT_TO=user@example.com
|
|
GARC_DEFAULT_AGENT=main
|
|
EOF
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
_profile_list() {
|
|
if [[ ! -d "${GARC_PROFILES_DIR}" ]]; then
|
|
echo "No profiles configured. Create one with: garc profile add <name>"
|
|
return 0
|
|
fi
|
|
|
|
local current="${GARC_PROFILE:-}"
|
|
echo "Profiles (dir: ${GARC_PROFILES_DIR}):"
|
|
echo ""
|
|
local found=0
|
|
for dir in "${GARC_PROFILES_DIR}"/*/; do
|
|
[[ -d "${dir}" ]] || continue
|
|
local name
|
|
name="$(basename "${dir}")"
|
|
local marker=""
|
|
[[ "${name}" == "${current}" ]] && marker=" ◀ active"
|
|
local has_token=""
|
|
[[ -f "${dir}/token.json" ]] && has_token=" [authenticated]"
|
|
local has_config=""
|
|
[[ -f "${dir}/config.env" ]] && has_config=" [configured]"
|
|
echo " ${name}${marker}${has_token}${has_config}"
|
|
found=1
|
|
done
|
|
[[ ${found} -eq 0 ]] && echo " (none)"
|
|
}
|
|
|
|
_profile_current() {
|
|
if [[ -n "${GARC_PROFILE:-}" ]]; then
|
|
echo "${GARC_PROFILE}"
|
|
else
|
|
echo "(no profile active — using ~/.garc/config.env)"
|
|
fi
|
|
}
|
|
|
|
_profile_use() {
|
|
local name="${1:-}"
|
|
if [[ -z "${name}" ]]; then
|
|
echo "Usage: garc profile use <name>"
|
|
return 1
|
|
fi
|
|
|
|
local profile_dir="${GARC_PROFILES_DIR}/${name}"
|
|
if [[ ! -d "${profile_dir}" ]]; then
|
|
echo "Profile '${name}' not found. Create it with: garc profile add ${name}"
|
|
return 1
|
|
fi
|
|
|
|
# Output shell export commands so caller can eval them
|
|
echo "export GARC_PROFILE=${name}"
|
|
if [[ -f "${profile_dir}/config.env" ]]; then
|
|
# Read each line and export
|
|
while IFS= read -r line || [[ -n "${line}" ]]; do
|
|
# Skip comments and empty lines
|
|
[[ "${line}" =~ ^[[:space:]]*# ]] && continue
|
|
[[ -z "${line// }" ]] && continue
|
|
echo "export ${line}"
|
|
done < "${profile_dir}/config.env"
|
|
fi
|
|
if [[ -f "${profile_dir}/token.json" ]]; then
|
|
echo "export GARC_TOKEN_FILE=${profile_dir}/token.json"
|
|
fi
|
|
if [[ -f "${profile_dir}/credentials.json" ]]; then
|
|
echo "export GARC_CREDENTIALS_FILE=${profile_dir}/credentials.json"
|
|
fi
|
|
|
|
# Print hint to stderr so it doesn't get eval'd
|
|
echo "# Activated profile: ${name}" >&2
|
|
echo "# Run: eval \"\$(garc profile use ${name})\"" >&2
|
|
}
|
|
|
|
_profile_add() {
|
|
local name="${1:-}"
|
|
if [[ -z "${name}" ]]; then
|
|
echo "Usage: garc profile add <name>"
|
|
return 1
|
|
fi
|
|
|
|
local profile_dir="${GARC_PROFILES_DIR}/${name}"
|
|
if [[ -d "${profile_dir}" ]]; then
|
|
echo "Profile '${name}' already exists: ${profile_dir}"
|
|
return 0
|
|
fi
|
|
|
|
mkdir -p "${profile_dir}"
|
|
|
|
cat > "${profile_dir}/config.env" <<EOF
|
|
# Profile: ${name}
|
|
# Fill in your tenant-specific values
|
|
|
|
GARC_DRIVE_FOLDER_ID=1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
GARC_SHEETS_ID=1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
GARC_GMAIL_DEFAULT_TO=you@example.com
|
|
GARC_DEFAULT_AGENT=main
|
|
# GARC_CHAT_SPACE_ID=spaces/xxxxxxxx
|
|
# GARC_APPROVAL_EMAIL=approver@example.com
|
|
EOF
|
|
|
|
echo "✅ Profile created: ${profile_dir}"
|
|
echo ""
|
|
echo "Next steps:"
|
|
echo " 1. Edit: ${profile_dir}/config.env"
|
|
echo " 2. Login: GARC_TOKEN_FILE=${profile_dir}/token.json garc auth login"
|
|
echo " 3. Activate: eval \"\$(garc profile use ${name})\""
|
|
}
|
|
|
|
_profile_show() {
|
|
local name="${1:-${GARC_PROFILE:-}}"
|
|
if [[ -z "${name}" ]]; then
|
|
echo "Usage: garc profile show <name>"
|
|
echo " (or set GARC_PROFILE to use current profile)"
|
|
return 1
|
|
fi
|
|
|
|
local profile_dir="${GARC_PROFILES_DIR}/${name}"
|
|
if [[ ! -d "${profile_dir}" ]]; then
|
|
echo "Profile '${name}' not found."
|
|
return 1
|
|
fi
|
|
|
|
echo "Profile: ${name}"
|
|
echo "Path: ${profile_dir}"
|
|
echo ""
|
|
if [[ -f "${profile_dir}/config.env" ]]; then
|
|
echo "config.env:"
|
|
grep -v '^#' "${profile_dir}/config.env" | grep -v '^[[:space:]]*$' \
|
|
| sed 's/^/ /'
|
|
else
|
|
echo " (no config.env)"
|
|
fi
|
|
echo ""
|
|
echo "Files:"
|
|
[[ -f "${profile_dir}/token.json" ]] && echo " ✅ token.json (authenticated)" || echo " ⬜ token.json (not authenticated)"
|
|
[[ -f "${profile_dir}/credentials.json" ]] && echo " ✅ credentials.json" || echo " ⬜ credentials.json (using default)"
|
|
[[ -f "${profile_dir}/service_account.json" ]] && echo " ✅ service_account.json"
|
|
}
|
|
|
|
_profile_remove() {
|
|
local name="${1:-}"
|
|
if [[ -z "${name}" ]]; then
|
|
echo "Usage: garc profile remove <name>"
|
|
return 1
|
|
fi
|
|
|
|
local profile_dir="${GARC_PROFILES_DIR}/${name}"
|
|
if [[ ! -d "${profile_dir}" ]]; then
|
|
echo "Profile '${name}' not found."
|
|
return 1
|
|
fi
|
|
|
|
echo "Remove profile '${name}'? This deletes: ${profile_dir} [y/N]"
|
|
read -r confirm
|
|
[[ "${confirm}" != "y" ]] && echo "Cancelled." && return 0
|
|
|
|
rm -rf "${profile_dir}"
|
|
echo "✅ Profile '${name}' removed."
|
|
}
|