gstack/bin/gstack-paths

107 lines
4.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# gstack-paths — output portable state-root paths for skill bash blocks
# Usage: eval "$(gstack-paths)" → sets GSTACK_STATE_ROOT, PLAN_ROOT, TMP_ROOT
# gstack-paths → prints GSTACK_STATE_ROOT=... etc.
# gstack-paths --get <key> → prints a single resolved path (no KEY= prefix)
# where <key> is state-root | plan-root | tmp-root
#
# The --get form was added so callers can use `PLAN_ROOT=$(gstack-paths --get plan-root)`
# instead of `eval "$(gstack-paths)"`. The eval form trips Claude Code's PreToolUse
# bash AST parser (tilde inside $() inside double-quoted eval arg = "Unhandled node
# type: string"), forcing manual approval on every skill that needs portable roots.
# See issue #1329 Pattern 2.
#
# Resolves three roots with explicit fallback chains so skills work the same
# whether installed as a Claude Code plugin (CLAUDE_PLUGIN_DATA / CLAUDE_PLANS_DIR
# set), a global ~/.claude/skills/gstack/ install, or a local checkout under
# CI / container env where HOME may be unset.
#
# Chains:
# GSTACK_STATE_ROOT: GSTACK_HOME -> CLAUDE_PLUGIN_DATA (only when CLAUDE_PLUGIN_ROOT=*gstack*) -> $HOME/.gstack -> .gstack
# PLAN_ROOT: GSTACK_PLAN_DIR -> CLAUDE_PLANS_DIR -> $HOME/.claude/plans -> .claude/plans
# TMP_ROOT: TMPDIR -> TMP -> .gstack/tmp (and mkdir -p, best-effort)
#
# Security: output values are not sanitized — callers may receive paths with
# shell-special characters if env vars contain them. Skills should always quote
# expansions ("$GSTACK_STATE_ROOT", not $GSTACK_STATE_ROOT).
set -u
# Parse optional --get <key> selector.
_get_key=""
case "${1:-}" in
--help|-h)
sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
--get)
if [ -z "${2:-}" ]; then
echo "gstack-paths: --get requires a key (state-root|plan-root|tmp-root)" >&2
exit 1
fi
_get_key="$2"
;;
"" ) ;; # bare invocation — backward-compatible default output
*)
echo "gstack-paths: unknown argument '$1' (expected --get <key>)" >&2
exit 1
;;
esac
# State root: where gstack writes projects/, sessions/, analytics/.
if [ -n "${GSTACK_HOME:-}" ]; then
_state_root="$GSTACK_HOME"
elif [ -n "${CLAUDE_PLUGIN_DATA:-}" ] && echo "${CLAUDE_PLUGIN_ROOT:-}" | grep -qi "gstack"; then
# Guard: only trust CLAUDE_PLUGIN_DATA when CLAUDE_PLUGIN_ROOT confirms we are
# running as the gstack plugin. Without this, a CLAUDE_PLUGIN_DATA from another
# plugin (e.g. codex) that leaked into the session env via CLAUDE_ENV_FILE would
# be picked up, writing all gstack state into the wrong directory.
_state_root="$CLAUDE_PLUGIN_DATA"
elif [ -n "${HOME:-}" ]; then
_state_root="$HOME/.gstack"
else
_state_root=".gstack"
fi
# Plan root: where /context-save and /codex consult write plan files.
if [ -n "${GSTACK_PLAN_DIR:-}" ]; then
_plan_root="$GSTACK_PLAN_DIR"
elif [ -n "${CLAUDE_PLANS_DIR:-}" ]; then
_plan_root="$CLAUDE_PLANS_DIR"
elif [ -n "${HOME:-}" ]; then
_plan_root="$HOME/.claude/plans"
else
_plan_root=".claude/plans"
fi
# Tmp root: where ephemeral files (codex stderr captures, etc.) live.
# Honor TMPDIR / TMP for Windows + container compat; fall back to a
# project-local .gstack/tmp so we never write to a system /tmp that may
# be read-only or shared.
if [ -n "${TMPDIR:-}" ]; then
_tmp_root="$TMPDIR"
elif [ -n "${TMP:-}" ]; then
_tmp_root="$TMP"
else
_tmp_root=".gstack/tmp"
fi
# Best-effort mkdir; if it fails (read-only fs, permission denied), the caller
# will discover that on their own write attempt. Don't fail the eval here.
mkdir -p "$_tmp_root" 2>/dev/null || true
if [ -n "$_get_key" ]; then
case "$_get_key" in
state-root) echo "$_state_root" ;;
plan-root) echo "$_plan_root" ;;
tmp-root) echo "$_tmp_root" ;;
*)
echo "gstack-paths: unknown key '$_get_key' (expected state-root|plan-root|tmp-root)" >&2
exit 1
;;
esac
else
echo "GSTACK_STATE_ROOT=$_state_root"
echo "PLAN_ROOT=$_plan_root"
echo "TMP_ROOT=$_tmp_root"
fi