- start.sh: auto-register project in ~/.config/context-studio/projects/ before launching Electron — without this acquireProjectLock() silently skips writing the lock file, waitForServers() never finds the registry port, all agent ports stay null (localhost:null errors) - start.sh: mount all known Claude Code credential locations into container (~/.claude/.credentials.json, ~/.claude.json, $CLAUDE_CONFIG_DIR variants) not just ~/.anthropic which was empty on this system - bin/claude: create /tmp/cs-ready-<agentId> on host after 3s delay so CS Core's CLI ready marker poll resolves instead of timing out after 10s - workflow.sh: add hasTrustDialogAccepted:true to all agent settings.json so claude goes straight to priming without the folder trust dialog - prereqs.sh: add ensure_api_key() — checks all credential locations, prompts with masked input if none found, offers to save to shell profile - wizard.sh: trap SIGINT for graceful abort — gum confirm popup, reverts created project dir and cloned core dir, leaves installed packages untouched - core.sh: set _WIZARD_CORE_CLONED=true before clone for cleanup tracking - electron-config.js: increase serverStartupTimeout 30s→90s (config file in core/config/, not source — safe to edit) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
291 lines
12 KiB
Bash
Executable file
291 lines
12 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
# ╔══════════════════════════════════════════════════════════════════╗
|
||
# ║ Context Studio Wizard — Project Setup ║
|
||
# ╚══════════════════════════════════════════════════════════════════╝
|
||
set -uo pipefail
|
||
|
||
WIZARD_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
|
||
# ── Bootstrap gum ────────────────────────────────────────────────────────
|
||
# Must happen before sourcing any lib that uses gum.
|
||
_install_gum() {
|
||
echo " Installing gum..."
|
||
if command -v pacman &>/dev/null; then
|
||
sudo pacman -Sy --noconfirm gum
|
||
elif command -v apt-get &>/dev/null; then
|
||
sudo mkdir -p /etc/apt/keyrings
|
||
curl -fsSL https://repo.charm.sh/apt/gpg.key \
|
||
| sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg
|
||
echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" \
|
||
| sudo tee /etc/apt/sources.list.d/charm.list >/dev/null
|
||
sudo apt-get update -qq && sudo apt-get install -y gum
|
||
elif command -v dnf &>/dev/null; then
|
||
sudo dnf install -y 'https://repo.charm.sh/rpm/charm-repo.rpm' 2>/dev/null || true
|
||
sudo dnf install -y gum
|
||
elif command -v yum &>/dev/null; then
|
||
sudo yum install -y 'https://repo.charm.sh/rpm/charm-repo.rpm' 2>/dev/null || true
|
||
sudo yum install -y gum
|
||
elif command -v zypper &>/dev/null; then
|
||
local ver="0.14.5"
|
||
curl -sL "https://github.com/charmbracelet/gum/releases/download/v${ver}/gum_${ver}_Linux_x86_64.tar.gz" \
|
||
| tar -xz -C /tmp gum
|
||
sudo mv /tmp/gum /usr/local/bin/gum
|
||
else
|
||
echo "Cannot install gum automatically."
|
||
echo "Install it manually: https://github.com/charmbracelet/gum"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
if ! command -v gum &>/dev/null; then
|
||
echo ""
|
||
echo " gum is required for the wizard UI (https://github.com/charmbracelet/gum)"
|
||
_install_gum
|
||
command -v gum &>/dev/null || { echo "gum installation failed."; exit 1; }
|
||
fi
|
||
|
||
# ── Source libs ──────────────────────────────────────────────────────────
|
||
source "$WIZARD_DIR/lib/utils.sh"
|
||
source "$WIZARD_DIR/lib/prereqs.sh"
|
||
source "$WIZARD_DIR/lib/core.sh"
|
||
source "$WIZARD_DIR/lib/project.sh"
|
||
source "$WIZARD_DIR/lib/workflow.sh"
|
||
source "$WIZARD_DIR/lib/container.sh"
|
||
|
||
# ── Find existing projects base dir ──────────────────────────────────────
|
||
find_projects_dir() {
|
||
local candidates=("Projects" "projects" "Project" "project" "Dev" "dev" "Workspace" "workspace" "Code" "code" "src" "Src")
|
||
for candidate in "${candidates[@]}"; do
|
||
if [[ -d "$HOME/$candidate" ]]; then
|
||
echo "$HOME/$candidate"
|
||
return
|
||
fi
|
||
done
|
||
echo "$HOME/Projects"
|
||
}
|
||
|
||
# ── Banner ────────────────────────────────────────────────────────────────
|
||
show_banner() {
|
||
clear
|
||
echo ""
|
||
gum style \
|
||
--border double \
|
||
--border-foreground "$C_MAUVE" \
|
||
--foreground "$C_MAUVE" \
|
||
--bold \
|
||
--align center \
|
||
--width 54 \
|
||
--margin "0 2" \
|
||
--padding "1 8" \
|
||
"🧙 Context Studio Wizard" \
|
||
"" \
|
||
"$(gum style --foreground "$C_SKY" --italic --bold "Scaffold your multi-agent AI")" \
|
||
"$(gum style --foreground "$C_SKY" --italic --bold "development environment")"
|
||
echo ""
|
||
}
|
||
|
||
# ── Project info ──────────────────────────────────────────────────────────
|
||
collect_project_info() {
|
||
header "Project Details"
|
||
|
||
ask PROJECT_NAME "Project name" ""
|
||
if [[ -z "$PROJECT_NAME" ]]; then die "Project name is required."; fi
|
||
|
||
local projects_base
|
||
projects_base="$(find_projects_dir)"
|
||
local default_dir="$projects_base/$(slugify "$PROJECT_NAME")"
|
||
ask PROJECT_DIR "Project location" "$default_dir"
|
||
if [[ -z "$PROJECT_DIR" ]]; then die "Project location is required."; fi
|
||
PROJECT_DIR="${PROJECT_DIR/#\~/$HOME}"
|
||
|
||
if [[ -e "$PROJECT_DIR" ]]; then
|
||
warn "Directory already exists: $PROJECT_DIR"
|
||
ask_yn _continue "Continue anyway?" "n"
|
||
if [[ "$_continue" != "y" ]]; then die "Aborted."; fi
|
||
fi
|
||
}
|
||
|
||
# ── Workflow source ───────────────────────────────────────────────────────
|
||
collect_workflow_info() {
|
||
header "Workflow Configuration"
|
||
|
||
ask_choice WORKFLOW_SOURCE "Workflow source" \
|
||
"Generate from scratch" \
|
||
"Clone from existing repo"
|
||
|
||
if [[ "$WORKFLOW_SOURCE" == "Clone from existing repo" ]]; then
|
||
ask WORKFLOW_REPO "Workflow repo URL" ""
|
||
if [[ -z "$WORKFLOW_REPO" ]]; then die "Repo URL is required."; fi
|
||
else
|
||
ask PROJECT_DESC "Project description" "A software project"
|
||
ask TECH_STACK "Tech stack" "Node.js"
|
||
ask_choice AGENT_PRESET "Agent preset" \
|
||
"minimal — 5 agents (coordinator · 2× coder · researcher · tester)" \
|
||
"standard — 9 agents (2× coordinator · 3× coder · 2× researcher · tester · reviewer)"
|
||
case "$AGENT_PRESET" in
|
||
minimal*) AGENT_PRESET="minimal" ;;
|
||
*) AGENT_PRESET="standard" ;;
|
||
esac
|
||
fi
|
||
}
|
||
|
||
# ── Summary ───────────────────────────────────────────────────────────────
|
||
confirm_summary() {
|
||
header "Summary"
|
||
|
||
local workflow_line
|
||
if [[ "${WORKFLOW_SOURCE:-}" == "Clone from existing repo" ]]; then
|
||
workflow_line="clone ${WORKFLOW_REPO:-}"
|
||
else
|
||
workflow_line="generate (${AGENT_PRESET:-minimal} preset)"
|
||
fi
|
||
|
||
gum style \
|
||
--border rounded \
|
||
--border-foreground "$C_MAUVE" \
|
||
--padding "1 3" \
|
||
--margin "0 2" \
|
||
"$(gum style --foreground "$C_SKY" --bold "Project") $(gum style --foreground "$C_TEXT" "$PROJECT_NAME")" \
|
||
"$(gum style --foreground "$C_SKY" --bold "Location") $(gum style --foreground "$C_TEXT" "$PROJECT_DIR")" \
|
||
"$(gum style --foreground "$C_SKY" --bold "Core") $(gum style --foreground "$C_TEXT" "$CS_CORE_DIR")" \
|
||
"$(gum style --foreground "$C_SKY" --bold "Workflow") $(gum style --foreground "$C_TEXT" "$workflow_line")"
|
||
|
||
echo ""
|
||
ask_yn _ok "Create project?" "y"
|
||
if [[ "$_ok" != "y" ]]; then die "Aborted."; fi
|
||
}
|
||
|
||
# ── Cleanup state tracking ────────────────────────────────────────────────
|
||
_WIZARD_CORE_CLONED=false
|
||
_WIZARD_PROJECT_CREATED=false
|
||
|
||
wizard_cleanup() {
|
||
local removed=false
|
||
if [[ "$_WIZARD_PROJECT_CREATED" == "true" && -n "${PROJECT_DIR:-}" && -d "$PROJECT_DIR" ]]; then
|
||
rm -rf "$PROJECT_DIR"
|
||
gum style --foreground "$C_GREEN" " ✓ Removed $PROJECT_DIR"
|
||
removed=true
|
||
fi
|
||
if [[ "$_WIZARD_CORE_CLONED" == "true" && -d "$CS_CORE_DIR" ]]; then
|
||
rm -rf "$CS_CORE_DIR"
|
||
gum style --foreground "$C_GREEN" " ✓ Removed $CS_CORE_DIR"
|
||
removed=true
|
||
fi
|
||
if [[ "$removed" == "false" ]]; then
|
||
gum style --foreground "$C_SURFACE" " Nothing to revert."
|
||
fi
|
||
}
|
||
|
||
handle_sigint() {
|
||
trap '' INT # block further Ctrl+C while dialog is open
|
||
echo ""
|
||
if gum confirm \
|
||
--affirmative "Yes, quit" \
|
||
--negative "No, continue" \
|
||
--default=No \
|
||
--prompt.foreground "$C_YELLOW" \
|
||
--selected.background "$C_RED" \
|
||
--selected.foreground "$C_BASE" \
|
||
--unselected.foreground "$C_TEXT" \
|
||
" Abort setup and revert changes?"; then
|
||
echo ""
|
||
gum style --foreground "$C_YELLOW" --bold " Reverting changes..."
|
||
wizard_cleanup
|
||
echo ""
|
||
gum style --foreground "$C_RED" --bold " Setup aborted."
|
||
echo ""
|
||
exit 130
|
||
fi
|
||
trap 'handle_sigint' INT # re-arm after "No, continue"
|
||
}
|
||
|
||
# ── Build ─────────────────────────────────────────────────────────────────
|
||
build_project() {
|
||
header "Building Project"
|
||
|
||
_WIZARD_PROJECT_CREATED=true
|
||
|
||
local slug
|
||
slug="$(slugify "$PROJECT_NAME")"
|
||
|
||
create_project_structure "$PROJECT_DIR" "$PROJECT_NAME"
|
||
create_devcontainer "$PROJECT_DIR" "$PROJECT_NAME"
|
||
generate_container_scripts "$PROJECT_DIR" "$PROJECT_NAME" "$slug"
|
||
|
||
if [[ "${WORKFLOW_SOURCE:-}" == "Clone from existing repo" ]]; then
|
||
clone_workflow "$PROJECT_DIR" "${WORKFLOW_REPO:-}"
|
||
else
|
||
generate_workflow "$PROJECT_DIR" "$PROJECT_NAME" \
|
||
"${PROJECT_DESC:-A software project}" \
|
||
"${TECH_STACK:-Node.js}" \
|
||
"${AGENT_PRESET:-minimal}"
|
||
fi
|
||
|
||
spin "Initializing git repo..." bash -c "
|
||
git -C '$PROJECT_DIR' init -q
|
||
git -C '$PROJECT_DIR' add .
|
||
git -C '$PROJECT_DIR' commit -q -m 'Initial project setup via Context Studio Wizard'
|
||
"
|
||
success "Git repo initialized"
|
||
}
|
||
|
||
# ── Done ──────────────────────────────────────────────────────────────────
|
||
print_next_steps() {
|
||
local slug
|
||
slug="$(slugify "$PROJECT_NAME")"
|
||
|
||
echo ""
|
||
gum style \
|
||
--border double \
|
||
--border-foreground "$C_GREEN" \
|
||
--foreground "$C_GREEN" \
|
||
--bold \
|
||
--align center \
|
||
--width 54 \
|
||
--margin "0 2" \
|
||
--padding "1 4" \
|
||
"✓ Project Created Successfully"
|
||
|
||
echo ""
|
||
gum style --foreground "$C_MAUVE" --bold --margin "0 2" " ➜ Enter your project:"
|
||
gum style --foreground "$C_SKY" --bold --margin "0 4" "cd \"$PROJECT_DIR\""
|
||
|
||
echo ""
|
||
gum style --foreground "$C_MAUVE" --bold --margin "0 2" " Available commands:"
|
||
echo ""
|
||
gum style --margin "0 4" \
|
||
"$(gum style --foreground "$C_SKY" --bold "./start.sh") — start container + Context Studio" \
|
||
"$(gum style --foreground "$C_SKY" --bold "./stop.sh") — stop the agents container" \
|
||
"$(gum style --foreground "$C_SKY" --bold "./update.sh") — update core, claude-code, OS packages"
|
||
|
||
echo ""
|
||
gum style \
|
||
--border normal \
|
||
--border-foreground "$C_YELLOW" \
|
||
--foreground "$C_YELLOW" \
|
||
--padding "0 2" \
|
||
--margin "0 2" \
|
||
"⚠ First run: start.sh builds the container image." \
|
||
" Expect 5–15 min on first start. Subsequent starts are instant."
|
||
|
||
echo ""
|
||
gum style --foreground "$C_SURFACE" --margin "0 2" \
|
||
"VS Code: code \"$PROJECT_DIR\" → Reopen in Container"
|
||
echo ""
|
||
}
|
||
|
||
# ── Main ──────────────────────────────────────────────────────────────────
|
||
main() {
|
||
trap 'handle_sigint' INT
|
||
show_banner
|
||
check_prerequisites
|
||
setup_core
|
||
collect_project_info
|
||
collect_workflow_info
|
||
confirm_summary
|
||
build_project
|
||
print_next_steps
|
||
trap - INT
|
||
}
|
||
|
||
main "$@"
|