From 1f3cd2a6472fb7e62c888bbc5dfe7517e3368142 Mon Sep 17 00:00:00 2001 From: "liguo.yang" Date: Fri, 1 May 2026 09:21:42 +0800 Subject: [PATCH] feat: setup --choose for interactive skill selection Add --choose flag to ./setup that shows an interactive multi-select menu, letting users pick which skills to install instead of installing all 45. - Pure bash menu, zero external dependencies - Works with all hosts (--host codex --choose) - Non-TTY environments get clear error - Default behavior unchanged (all selected) --- setup | 215 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 200 insertions(+), 15 deletions(-) diff --git a/setup b/setup index 4c1763f9fd..358e4a9086 100755 --- a/setup +++ b/setup @@ -41,6 +41,7 @@ SKILL_PREFIX=1 SKILL_PREFIX_FLAG=0 TEAM_MODE=0 NO_TEAM_MODE=0 +CHOOSE_MODE=0 while [ $# -gt 0 ]; do case "$1" in --host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, or auto)" >&2 && exit 1; HOST="$2"; shift 2 ;; @@ -50,6 +51,7 @@ while [ $# -gt 0 ]; do --no-prefix) SKILL_PREFIX=0; SKILL_PREFIX_FLAG=1; shift ;; --team) TEAM_MODE=1; shift ;; --no-team) NO_TEAM_MODE=1; shift ;; + --choose) CHOOSE_MODE=1; shift ;; -q|--quiet) QUIET=1; shift ;; *) shift ;; esac @@ -373,12 +375,20 @@ mkdir -p "$HOME/.gstack/projects" link_claude_skill_dirs() { local gstack_dir="$1" local skills_dir="$2" + local filter_list="${3:-}" local linked=() for skill_dir in "$gstack_dir"/*/; do if [ -f "$skill_dir/SKILL.md" ]; then dir_name="$(basename "$skill_dir")" # Skip node_modules [ "$dir_name" = "node_modules" ] && continue + # --choose filter: skip skills not in selected list + if [ -n "$filter_list" ]; then + case " $filter_list " in + *" $dir_name "*) ;; + *) continue ;; + esac + fi # Use frontmatter name: if present (e.g., run-tests/ with name: test → symlink as "test") skill_name=$(grep -m1 '^name:' "$skill_dir/SKILL.md" 2>/dev/null | sed 's/^name:[[:space:]]*//' | tr -d '[:space:]') [ -z "$skill_name" ] && skill_name="$dir_name" @@ -491,12 +501,123 @@ cleanup_prefixed_claude_symlinks() { fi } +# ─── Interactive skill picker for --choose mode ────────────────────────── +# Discovers all skill dirs (containing SKILL.md) and presents a multi-select menu. +# Sets SELECTED_SKILLS array with chosen skill directory names. +choose_skills() { + local gstack_dir="$1" + local all_skills=() + local selected_flags=() + + # Discover skills + for dir in "$gstack_dir"/*/; do + [ -f "$dir/SKILL.md" ] || continue + local name; name="$(basename "$dir")" + case "$name" in + .agents|node_modules|.git|.github) continue ;; + esac + all_skills+=("$name") + done + + local total=${#all_skills[@]} + if [ "$total" -eq 0 ]; then + echo " No skills found in $gstack_dir" >&2 + return 1 + fi + + # Default: all selected + for (( i=0; i&2 + valid=0 + break + fi + done + [ "$valid" -eq 0 ] && continue + ;; + esac + print_menu + done + + # Collect results + SELECTED_SKILLS=() + for (( i=0; i&2 + exit 1 + fi + choose_skills "$SOURCE_GSTACK_DIR" + CHOOSE_FILTER="${SELECTED_SKILLS[*]}" +fi + SKILLS_BASENAME="$(basename "$INSTALL_SKILLS_DIR")" SKILLS_PARENT_BASENAME="$(basename "$(dirname "$INSTALL_SKILLS_DIR")")" CODEX_REPO_LOCAL=0 @@ -782,7 +940,7 @@ if [ "$INSTALL_CLAUDE" -eq 1 ]; then # Patch name: fields BEFORE creating symlinks so link_claude_skill_dirs # reads the correct (patched) name: values for symlink naming "$SOURCE_GSTACK_DIR/bin/gstack-patch-names" "$SOURCE_GSTACK_DIR" "$SKILL_PREFIX" - link_claude_skill_dirs "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR" + link_claude_skill_dirs "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR" "$CHOOSE_FILTER" # Self-healing: re-run gstack-relink to ensure name: fields and directory # names are consistent with the config. This catches cases where an interrupted # setup, stale git state, or gen:skill-docs left name: fields out of sync. @@ -791,12 +949,22 @@ if [ "$INSTALL_CLAUDE" -eq 1 ]; then GSTACK_SKILLS_DIR="$INSTALL_SKILLS_DIR" GSTACK_INSTALL_DIR="$SOURCE_GSTACK_DIR" "$GSTACK_RELINK" >/dev/null 2>&1 || true fi # Backwards-compat alias: /connect-chrome → /open-gstack-browser - _OGB_LINK="$INSTALL_SKILLS_DIR/connect-chrome" - if [ "$SKILL_PREFIX" -eq 1 ]; then - _OGB_LINK="$INSTALL_SKILLS_DIR/gstack-connect-chrome" + # Skip if open-gstack-browser was filtered out by --choose + _SKIP_OGB_ALIAS=0 + if [ -n "$CHOOSE_FILTER" ]; then + case " $CHOOSE_FILTER " in + *" open-gstack-browser "*) ;; + *) _SKIP_OGB_ALIAS=1 ;; + esac fi - if [ -L "$_OGB_LINK" ] || [ ! -e "$_OGB_LINK" ]; then - ln -snf "gstack/open-gstack-browser" "$_OGB_LINK" + if [ "$_SKIP_OGB_ALIAS" -eq 0 ]; then + _OGB_LINK="$INSTALL_SKILLS_DIR/connect-chrome" + if [ "$SKILL_PREFIX" -eq 1 ]; then + _OGB_LINK="$INSTALL_SKILLS_DIR/gstack-connect-chrome" + fi + if [ -L "$_OGB_LINK" ] || [ ! -e "$_OGB_LINK" ]; then + ln -snf "gstack/open-gstack-browser" "$_OGB_LINK" + fi fi if [ "$LOCAL_INSTALL" -eq 1 ]; then log "gstack ready (project-local)." @@ -821,17 +989,26 @@ if [ "$INSTALL_CLAUDE" -eq 1 ]; then cleanup_prefixed_claude_symlinks "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR" fi "$SOURCE_GSTACK_DIR/bin/gstack-patch-names" "$SOURCE_GSTACK_DIR" "$SKILL_PREFIX" - link_claude_skill_dirs "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR" + link_claude_skill_dirs "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR" "$CHOOSE_FILTER" GSTACK_RELINK="$SOURCE_GSTACK_DIR/bin/gstack-relink" if [ -x "$GSTACK_RELINK" ]; then GSTACK_SKILLS_DIR="$INSTALL_SKILLS_DIR" GSTACK_INSTALL_DIR="$SOURCE_GSTACK_DIR" "$GSTACK_RELINK" >/dev/null 2>&1 || true fi - _OGB_LINK="$INSTALL_SKILLS_DIR/connect-chrome" - if [ "$SKILL_PREFIX" -eq 1 ]; then - _OGB_LINK="$INSTALL_SKILLS_DIR/gstack-connect-chrome" + _SKIP_OGB_ALIAS=0 + if [ -n "$CHOOSE_FILTER" ]; then + case " $CHOOSE_FILTER " in + *" open-gstack-browser "*) ;; + *) _SKIP_OGB_ALIAS=1 ;; + esac fi - if [ -L "$_OGB_LINK" ] || [ ! -e "$_OGB_LINK" ]; then - ln -snf "gstack/open-gstack-browser" "$_OGB_LINK" + if [ "$_SKIP_OGB_ALIAS" -eq 0 ]; then + _OGB_LINK="$INSTALL_SKILLS_DIR/connect-chrome" + if [ "$SKILL_PREFIX" -eq 1 ]; then + _OGB_LINK="$INSTALL_SKILLS_DIR/gstack-connect-chrome" + fi + if [ -L "$_OGB_LINK" ] || [ ! -e "$_OGB_LINK" ]; then + ln -snf "gstack/open-gstack-browser" "$_OGB_LINK" + fi fi log "gstack ready (claude)." log " browse: $BROWSE_BIN" @@ -852,7 +1029,7 @@ if [ "$INSTALL_CODEX" -eq 1 ]; then create_codex_runtime_root "$SOURCE_GSTACK_DIR" "$CODEX_GSTACK" fi # Install generated Codex-format skills (not Claude source dirs) - link_codex_skill_dirs "$SOURCE_GSTACK_DIR" "$CODEX_SKILLS" + link_codex_skill_dirs "$SOURCE_GSTACK_DIR" "$CODEX_SKILLS" "$CHOOSE_FILTER" log "gstack ready (codex)." log " browse: $BROWSE_BIN" @@ -900,6 +1077,14 @@ if [ "$INSTALL_KIRO" -eq 1 ]; then for skill_dir in "$AGENTS_DIR"/gstack*/; do [ -f "$skill_dir/SKILL.md" ] || continue skill_name="$(basename "$skill_dir")" + # --choose filter + if [ -n "$CHOOSE_FILTER" ]; then + _bare="${skill_name#gstack-}" + case " $CHOOSE_FILTER " in + *" $_bare "*) ;; + *) continue ;; + esac + fi target_dir="$KIRO_SKILLS/$skill_name" mkdir -p "$target_dir" # Generated Codex skills use $HOME/.codex (not ~/), plus $GSTACK_ROOT variables. @@ -919,7 +1104,7 @@ fi if [ "$INSTALL_FACTORY" -eq 1 ]; then mkdir -p "$FACTORY_SKILLS" create_factory_runtime_root "$SOURCE_GSTACK_DIR" "$FACTORY_GSTACK" - link_factory_skill_dirs "$SOURCE_GSTACK_DIR" "$FACTORY_SKILLS" + link_factory_skill_dirs "$SOURCE_GSTACK_DIR" "$FACTORY_SKILLS" "$CHOOSE_FILTER" echo "gstack ready (factory)." echo " browse: $BROWSE_BIN" echo " factory skills: $FACTORY_SKILLS" @@ -929,7 +1114,7 @@ fi if [ "$INSTALL_OPENCODE" -eq 1 ]; then mkdir -p "$OPENCODE_SKILLS" create_opencode_runtime_root "$SOURCE_GSTACK_DIR" "$OPENCODE_GSTACK" - link_opencode_skill_dirs "$SOURCE_GSTACK_DIR" "$OPENCODE_SKILLS" + link_opencode_skill_dirs "$SOURCE_GSTACK_DIR" "$OPENCODE_SKILLS" "$CHOOSE_FILTER" echo "gstack ready (opencode)." echo " browse: $BROWSE_BIN" echo " opencode skills: $OPENCODE_SKILLS"