Redesign wizard UI with gum (charmbracelet)

- Bootstrap gum automatically on first run (Arch/Debian/RHEL/Fedora/SUSE)
- utils.sh: replace all bash color helpers with gum equivalents
  - gum input for text prompts (with value pre-fill for defaults)
  - gum choose for selection menus
  - gum confirm for yes/no
  - gum spin for long-running operations
  - gum style/log for output (catppuccin mocha palette)
  - gum style for banners and summary box
- core.sh: spinner on git clone/pull
- workflow.sh: spinner on git clone
- prereqs.sh: spinner on package installs
- wizard.sh: double-border welcome banner, rounded summary box,
  success banner with next-steps panel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Eli 2026-03-09 13:02:42 +01:00
commit 699087f08c
6 changed files with 267 additions and 243 deletions

View file

@ -9,12 +9,11 @@ setup_core() {
header "Context Studio Core"
if [[ -d "$CS_CORE_DIR/.git" ]]; then
success "Core already installed at $CS_CORE_DIR"
ask_yn _update "Update core to latest?" "n"
success "Core installed at $CS_CORE_DIR"
ask_yn _update "Pull latest updates?" "n"
if [[ "$_update" == "y" ]]; then
info "Pulling latest core..."
git -C "$CS_CORE_DIR" pull --ff-only \
|| warn "Pull failed — continuing with existing version"
spin "Updating core..." git -C "$CS_CORE_DIR" pull --ff-only \
|| warn "Pull failed — continuing with current version."
success "Core updated"
fi
return 0
@ -22,7 +21,8 @@ setup_core() {
info "Cloning context-studio-core → $CS_CORE_DIR"
mkdir -p "$CS_HOME"
git clone "$CS_CORE_REPO" "$CS_CORE_DIR" \
spin "Cloning context-studio-core..." \
git clone "$CS_CORE_REPO" "$CS_CORE_DIR" \
|| die "Failed to clone context-studio-core. Check your SSH key and network."
success "Core installed at $CS_CORE_DIR"
}

View file

@ -1,13 +1,12 @@
#!/usr/bin/env bash
# prereqs.sh — detect distro, install missing prerequisites
# Detect package manager / distro family
detect_distro() {
if command -v pacman &>/dev/null; then echo "arch";
if command -v pacman &>/dev/null; then echo "arch";
elif command -v apt-get &>/dev/null; then echo "debian";
elif command -v dnf &>/dev/null; then echo "rhel";
elif command -v yum &>/dev/null; then echo "rhel-yum";
elif command -v zypper &>/dev/null; then echo "suse";
elif command -v dnf &>/dev/null; then echo "rhel";
elif command -v yum &>/dev/null; then echo "rhel-yum";
elif command -v zypper &>/dev/null; then echo "suse";
else echo "unknown";
fi
}
@ -16,130 +15,91 @@ install_pkg() {
local pkg="$1"
local distro
distro="$(detect_distro)"
info "Installing $pkg (distro: $distro)..."
case "$distro" in
arch)
sudo pacman -Sy --noconfirm "$pkg" ;;
debian)
sudo apt-get update -qq && sudo apt-get install -y "$pkg" ;;
rhel)
sudo dnf install -y "$pkg" ;;
rhel-yum)
sudo yum install -y "$pkg" ;;
suse)
sudo zypper install -y "$pkg" ;;
*)
die "Unsupported distro — please install $pkg manually." ;;
arch) sudo pacman -Sy --noconfirm "$pkg" ;;
debian) sudo apt-get update -qq && sudo apt-get install -y "$pkg" ;;
rhel) sudo dnf install -y "$pkg" ;;
rhel-yum) sudo yum install -y "$pkg" ;;
suse) sudo zypper install -y "$pkg" ;;
*) die "Unsupported distro — please install $pkg manually." ;;
esac
}
ensure_git() {
if command -v git &>/dev/null; then
success "git: $(git --version)"
success "git $(git --version | awk '{print $3}')"
return
fi
warn "git not found."
ask_yn _install "Install git now?" "y"
if [[ "$_install" != "y" ]]; then die "git is required."; fi
install_pkg git
gum spin --spinner dot --spinner.foreground "$C_MAUVE" \
--title " Installing git..." --title.foreground "$C_SKY" \
-- bash -c "$(declare -f install_pkg detect_distro); install_pkg git"
command -v git &>/dev/null || die "git installation failed."
success "git installed: $(git --version)"
success "git $(git --version | awk '{print $3}')"
}
ensure_container_runtime() {
# Already available?
if command -v podman &>/dev/null; then
CONTAINER_CMD="podman"
success "podman: $(podman --version)"
success "podman $(podman --version | awk '{print $3}')"
return
fi
if command -v docker &>/dev/null; then
CONTAINER_CMD="docker"
success "docker: $(docker --version | head -1)"
success "docker $(docker --version | awk '{print $3}' | tr -d ',')"
return
fi
warn "No container runtime found (podman or docker)."
warn "No container runtime found."
echo ""
echo -e " ${CYAN}podman${RESET} is preferred (rootless, no daemon)"
echo -e " ${CYAN}docker${RESET} is the alternative"
gum style --foreground "$C_SKY" --margin "0 4" \
"podman — recommended (rootless, no daemon)" \
"docker — alternative"
echo ""
ask_choice _runtime "Which would you like to install?" \
"podman (recommended)" \
"docker"
local runtime_choice="$_runtime"
case "$runtime_choice" in
podman*)
_install_podman
CONTAINER_CMD="podman"
;;
docker*)
_install_docker
CONTAINER_CMD="docker"
;;
podman*) _install_podman; CONTAINER_CMD="podman" ;;
docker*) _install_docker; CONTAINER_CMD="docker" ;;
esac
command -v "$CONTAINER_CMD" &>/dev/null \
|| die "$CONTAINER_CMD installation failed. Please install manually."
success "$CONTAINER_CMD installed: $($CONTAINER_CMD --version | head -1)"
success "$CONTAINER_CMD installed"
}
_install_podman() {
local distro
distro="$(detect_distro)"
case "$distro" in
arch) install_pkg podman ;;
debian) install_pkg podman ;;
rhel) install_pkg podman ;;
rhel-yum) install_pkg podman ;;
suse) install_pkg podman ;;
*) die "Unsupported distro — install podman manually: https://podman.io/getting-started/installation" ;;
esac
gum spin --spinner dot --spinner.foreground "$C_MAUVE" \
--title " Installing podman..." --title.foreground "$C_SKY" \
-- bash -c "$(declare -f install_pkg detect_distro); install_pkg podman"
}
_install_docker() {
local distro
distro="$(detect_distro)"
case "$distro" in
arch)
install_pkg docker
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"
warn "Added $USER to docker group — log out and back in for it to take effect."
;;
debian)
install_pkg docker.io
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"
warn "Added $USER to docker group — log out and back in for it to take effect."
;;
rhel)
install_pkg docker
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"
warn "Added $USER to docker group — log out and back in for it to take effect."
;;
rhel-yum)
install_pkg docker
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"
warn "Added $USER to docker group — log out and back in for it to take effect."
;;
suse)
install_pkg docker
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"
warn "Added $USER to docker group — log out and back in for it to take effect."
;;
*)
die "Unsupported distro — install docker manually: https://docs.docker.com/engine/install/" ;;
arch) install_pkg docker ;;
debian) install_pkg docker.io ;;
rhel) install_pkg docker ;;
rhel-yum) install_pkg docker ;;
suse) install_pkg docker ;;
*) die "Unsupported distro — install docker manually." ;;
esac
sudo systemctl enable --now docker 2>/dev/null || true
sudo usermod -aG docker "$USER" 2>/dev/null || true
warn "Added $USER to docker group — log out and back in for it to take effect."
}
check_prerequisites() {
header "Checking Prerequisites"
header "Prerequisites"
ensure_git
ensure_container_runtime
}

View file

@ -7,74 +7,24 @@ create_project_structure() {
local project_dir="$1"
local project_name="$2"
info "Creating project structure at $project_dir..."
mkdir -p "$project_dir/src"
mkdir -p "$project_dir/.devcontainer"
# .gitignore
cat > "$project_dir/.gitignore" <<'EOF'
# Dependencies
cat > "$project_dir/.gitignore" <<'GITIGNORE'
node_modules/
.pnp/
.pnp.js
# Build outputs
dist/
build/
*.AppImage
*.dmg
*.exe
# Environment
.env
.env.local
.env.*.local
# Runtime data
workflow/data/registry.db
workflow/users/*/session-history/
# Logs
*.log
npm-debug.log*
# OS
.DS_Store
Thumbs.db
EOF
# README
cat > "$project_dir/README.md" <<EOF
# $project_name
## Quick Start
### First time setup
\`\`\`bash
# Open in devcontainer (VS Code)
code .
# → Reopen in Container
# Or via CLI
docker build -t $( slugify "$project_name" ) .devcontainer/
docker run -it --rm \\
-v "\$(pwd)":/workspace \\
-v "\$HOME/.context-studio/core":/opt/context-studio/core \\
-e ANTHROPIC_API_KEY="\$ANTHROPIC_API_KEY" \\
$( slugify "$project_name" ) bash
\`\`\`
### Start Context Studio
\`\`\`bash
CS_WORKFLOW_DIR=/workspace/workflow node /opt/context-studio/core/core/start.js
\`\`\`
### Start headless (no Electron UI)
\`\`\`bash
CS_WORKFLOW_DIR=/workspace/workflow node /opt/context-studio/core/core/start.js --ui-mode=headless
\`\`\`
EOF
GITIGNORE
success "Project structure created"
}
@ -85,17 +35,11 @@ create_devcontainer() {
local slug
slug="$(slugify "$project_name")"
info "Generating devcontainer config..."
# Dockerfile
sed "s/{{PROJECT_SLUG}}/$slug/g" \
"$WIZARD_DIR/templates/Dockerfile" \
> "$project_dir/.devcontainer/Dockerfile"
# devcontainer.json
sed "s/{{PROJECT_NAME}}/$project_name/g; s/{{PROJECT_SLUG}}/$slug/g" \
"$WIZARD_DIR/templates/devcontainer.json" \
> "$project_dir/.devcontainer/devcontainer.json"
success "Devcontainer config written to $project_dir/.devcontainer/"
cp "$WIZARD_DIR/templates/Dockerfile" "$project_dir/.devcontainer/Dockerfile"
success "Devcontainer config written"
}

View file

@ -1,70 +1,119 @@
#!/usr/bin/env bash
# utils.sh — colors, prompts, helpers
# utils.sh — gum-based UI helpers (requires gum)
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'
# ── Catppuccin Mocha palette ─────────────────────────────────────────────
C_MAUVE="#CBA6F7"
C_SKY="#89DCEB"
C_GREEN="#A6E3A1"
C_YELLOW="#F9E2AF"
C_RED="#F38BA8"
C_PINK="#F5C2E7"
C_BASE="#1E1E2E"
C_TEXT="#CDD6F4"
C_SURFACE="#585B70"
info() { echo -e "${CYAN}${BOLD}[info]${RESET} $*"; }
success() { echo -e "${GREEN}${BOLD}[ok]${RESET} $*"; }
warn() { echo -e "${YELLOW}${BOLD}[warn]${RESET} $*"; }
error() { echo -e "${RED}${BOLD}[error]${RESET} $*" >&2; }
die() { error "$*"; exit 1; }
header() { echo -e "\n${BOLD}${CYAN}━━━ $* ━━━${RESET}\n"; }
# ── Output helpers ────────────────────────────────────────────────────────
info() { gum log --level info -- "$*"; }
success() { gum style --foreground "$C_GREEN" " $*"; }
warn() { gum log --level warn -- "$*"; }
error() { gum log --level error -- "$*" >&2; }
die() { gum style --foreground "$C_RED" --bold "$*" >&2; exit 1; }
# ask VAR "prompt" "default"
header() {
echo ""
gum style \
--foreground "$C_MAUVE" --bold \
--margin "0 2" \
"$*"
gum style \
--foreground "$C_SURFACE" \
--margin "0 2" \
"────────────────────────────────────────────────"
echo ""
}
# ── Prompts ───────────────────────────────────────────────────────────────
# ask VAR "Label" "default"
ask() {
local var="$1" prompt="$2" default="$3"
local input
local var="$1" prompt="$2" default="${3:-}"
local result
if [[ -n "$default" ]]; then
echo -ne "${BOLD}${prompt}${RESET} ${CYAN}[${default}]${RESET}: "
result=$(gum input \
--value "$default" \
--prompt " " \
--prompt.foreground "$C_MAUVE" \
--cursor.foreground "$C_MAUVE" \
--header " $prompt" \
--header.foreground "$C_SKY" \
--width 70) || true
else
echo -ne "${BOLD}${prompt}${RESET}: "
result=$(gum input \
--placeholder "(required)" \
--prompt " " \
--prompt.foreground "$C_MAUVE" \
--cursor.foreground "$C_MAUVE" \
--header " $prompt" \
--header.foreground "$C_SKY" \
--width 70) || true
fi
read -r input || true
if [[ -z "$input" && -n "$default" ]]; then
if [[ -z "$result" && -n "$default" ]]; then
eval "$var=\"\$default\""
else
eval "$var=\"\$input\""
eval "$var=\"\$result\""
fi
}
# ask_yn VAR "prompt" "y|n"
# ask_yn VAR "Question" "y|n"
ask_yn() {
local var="$1" prompt="$2" default="$3"
local input options
if [[ "$default" == "y" ]]; then options="Y/n"; else options="y/N"; fi
echo -ne "${BOLD}${prompt}${RESET} ${CYAN}[${options}]${RESET}: "
read -r input || true
input="${input:-$default}"
if [[ "$input" =~ ^[Yy]$ ]]; then
local affirmative="Yes" negative="No"
[[ "$default" == "y" ]] && affirmative="Yes" || affirmative="Yes"
if gum confirm \
--affirmative "Yes" \
--negative "No" \
--default="$([[ "$default" == "y" ]] && echo Yes || echo No)" \
--prompt.foreground "$C_SKY" \
--selected.background "$C_MAUVE" \
--selected.foreground "$C_BASE" \
--unselected.foreground "$C_TEXT" \
" $prompt"; then
eval "$var=y"
else
eval "$var=n"
fi
}
# ask_choice VAR "prompt" option1 option2 ...
# ask_choice VAR "Header" option1 option2 ...
ask_choice() {
local var="$1" prompt="$2"; shift 2
local options=("$@")
local input idx
echo -e "${BOLD}${prompt}${RESET}"
for i in "${!options[@]}"; do
echo -e " ${CYAN}$((i+1))${RESET}) ${options[$i]}"
done
echo -ne "Choice ${CYAN}[1]${RESET}: "
read -r input || true
input="${input:-1}"
if [[ "$input" =~ ^[0-9]+$ ]] && (( input >= 1 && input <= ${#options[@]} )); then
idx=$(( input - 1 ))
local first="$1"
local result
result=$(gum choose \
--cursor " " \
--cursor.foreground "$C_MAUVE" \
--selected.foreground "$C_MAUVE" \
--selected.bold \
--header " $prompt" \
--header.foreground "$C_SKY" \
--height 10 \
"$@") || true
if [[ -z "$result" ]]; then
eval "$var=\"\$first\""
else
idx=0
eval "$var=\"\$result\""
fi
eval "$var=\"\${options[$idx]}\""
}
# spin "Title" command args...
spin() {
local title="$1"; shift
gum spin \
--spinner dot \
--spinner.foreground "$C_MAUVE" \
--title " $title" \
--title.foreground "$C_SKY" \
-- "$@"
}
require_cmd() {

View file

@ -12,7 +12,7 @@ generate_workflow() {
local preset="$5"
local workflow_dir="$project_dir/workflow"
info "Generating workflow config ($preset preset)..."
info "Generating workflow ($preset preset)..."
mkdir -p "$workflow_dir/agents"
mkdir -p "$workflow_dir/roles"
@ -111,7 +111,7 @@ Read \`project-docs/project-vision.md\` for project goals.
Use \`/sm <agent> "message"\` to send messages between agents.
EOF
success "Workflow generated at $workflow_dir"
success "Workflow generated"
}
_create_agent_dir() {
@ -159,8 +159,8 @@ clone_workflow() {
local repo_url="$2"
local workflow_dir="$project_dir/workflow"
info "Cloning workflow from $repo_url..."
git clone "$repo_url" "$workflow_dir" \
spin "Cloning workflow..." \
git clone "$repo_url" "$workflow_dir" \
|| die "Failed to clone workflow repo: $repo_url"
success "Workflow cloned to $workflow_dir"
success "Workflow cloned"
}