Files
bw-copilot-resources/install/bootstrap.sh

268 lines
6.2 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
repo_root="$(cd -- "$script_dir/.." && pwd -P)"
canonical_home="${COPILOT_RESOURCES_HOME:-$HOME/.copilot-resources}"
state_dir="${COPILOT_RESOURCES_STATE_DIR:-$HOME/.copilot-resources-state}"
copilot_home="${COPILOT_HOME:-$HOME/.copilot}"
managed_shell_env=""
shell_rc_file=""
vscode_settings_file=""
usage() {
cat <<'EOF'
Usage: install/bootstrap.sh [--print-home]
Creates a stable canonical home for the repository and links default VS Code and
Copilot discovery paths back to the repository.
EOF
}
resolve_dir() {
cd -- "$1" && pwd -P
}
ensure_parent_dir() {
mkdir -p -- "$(dirname -- "$1")"
}
find_node_bin() {
if command -v node >/dev/null 2>&1; then
command -v node
return 0
fi
if command -v nodejs >/dev/null 2>&1; then
command -v nodejs
return 0
fi
return 1
}
link_path() {
local target="$1"
local link_path="$2"
local resolved_target
resolved_target="$(resolve_dir "$target")"
ensure_parent_dir "$link_path"
if [[ -L "$link_path" ]]; then
local existing_target
existing_target="$(cd -- "$(dirname -- "$link_path")" && resolve_dir "$(readlink "$link_path")")"
if [[ "$existing_target" == "$resolved_target" ]]; then
return 0
fi
printf 'Refusing to replace existing symlink %s -> %s\n' "$link_path" "$existing_target" >&2
return 1
fi
if [[ -e "$link_path" ]]; then
if [[ -d "$link_path" ]] && [[ -z "$(find "$link_path" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)" ]]; then
rmdir "$link_path"
else
printf 'Refusing to replace existing path: %s\n' "$link_path" >&2
return 1
fi
fi
ln -s -- "$target" "$link_path"
}
detect_vscode_user_dir() {
if [[ -n "${VSCODE_USER_DIR:-}" ]]; then
printf '%s\n' "$VSCODE_USER_DIR"
return 0
fi
case "$(uname -s)" in
Darwin)
printf '%s\n' "$HOME/Library/Application Support/Code/User"
;;
Linux)
printf '%s\n' "$HOME/.config/Code/User"
;;
*)
printf '%s\n' "$HOME/.config/Code/User"
;;
esac
}
detect_shell_rc_file() {
case "$(basename -- "${SHELL:-}")" in
zsh)
printf '%s\n' "$HOME/.zshrc"
;;
bash)
printf '%s\n' "$HOME/.bashrc"
;;
*)
printf '%s\n' "$HOME/.profile"
;;
esac
}
upsert_managed_block() {
local file_path="$1"
local begin_marker="$2"
local end_marker="$3"
local body="$4"
local temp_file
temp_file="$(mktemp)"
ensure_parent_dir "$file_path"
if [[ -f "$file_path" ]]; then
awk -v begin_marker="$begin_marker" -v end_marker="$end_marker" '
$0 == begin_marker { skip = 1; next }
$0 == end_marker { skip = 0; next }
!skip { print }
' "$file_path" > "$temp_file"
fi
if [[ -s "$temp_file" ]]; then
printf '\n' >> "$temp_file"
fi
cat >> "$temp_file" <<EOF
$begin_marker
$body
$end_marker
EOF
mv -- "$temp_file" "$file_path"
}
write_managed_shell_env() {
local shell_block
managed_shell_env="$state_dir/copilot-cli-env.sh"
ensure_parent_dir "$managed_shell_env"
cat > "$managed_shell_env" <<EOF
export COPILOT_RESOURCES_HOME="${canonical_home}"
export COPILOT_HOME="${copilot_home}"
export COPILOT_CUSTOM_INSTRUCTIONS_DIRS="${canonical_home}/resources/instructions"
EOF
shell_rc_file="$(detect_shell_rc_file)"
shell_block="$(cat <<EOF
if [ -f "$managed_shell_env" ]; then
. "$managed_shell_env"
fi
EOF
)"
upsert_managed_block \
"$shell_rc_file" \
"# >>> copilot-resources bootstrap >>>" \
"# <<< copilot-resources bootstrap <<<" \
"$shell_block"
}
merge_vscode_settings() {
local node_bin
vscode_settings_file="$vscode_user_dir/settings.json"
if ! node_bin="$(find_node_bin)"; then
printf 'Skipping VS Code settings merge because Node.js is not available.\n' >&2
return 0
fi
ensure_parent_dir "$vscode_settings_file"
"$node_bin" "$script_dir/merge-vscode-settings.mjs" \
--target "$vscode_settings_file" \
--template "$canonical_home/config/vscode/settings.template.jsonc" \
--set "COPILOT_RESOURCES_HOME=$canonical_home"
}
write_state() {
mkdir -p -- "$state_dir"
cat > "$state_dir/install-state.json" <<EOF
{
"canonicalHome": "${canonical_home}",
"repoRoot": "${repo_root}",
"copilotHome": "${copilot_home}",
"vscodeUserDir": "${vscode_user_dir}",
"vscodeSettingsFile": "${vscode_settings_file}",
"shellRcFile": "${shell_rc_file}",
"managedShellEnv": "${managed_shell_env}",
"bootstrapScript": "${script_dir}/bootstrap.sh"
}
EOF
}
main() {
if [[ "${1:-}" == "--help" ]]; then
usage
exit 0
fi
if [[ "${1:-}" == "--print-home" ]]; then
printf '%s\n' "$canonical_home"
exit 0
fi
local canonical_parent
canonical_parent="$(dirname -- "$canonical_home")"
mkdir -p -- "$canonical_parent"
if [[ -e "$canonical_home" ]]; then
if [[ "$(resolve_dir "$canonical_home")" != "$repo_root" ]]; then
printf 'Canonical path already exists and points elsewhere: %s\n' "$canonical_home" >&2
exit 1
fi
else
ln -s -- "$repo_root" "$canonical_home"
fi
mkdir -p -- "$copilot_home"
vscode_user_dir="$(detect_vscode_user_dir)"
mkdir -p -- "$vscode_user_dir"
link_path "$canonical_home/resources/skills" "$copilot_home/skills"
link_path "$canonical_home/resources/agents" "$copilot_home/agents"
link_path "$canonical_home/resources/instructions" "$copilot_home/instructions"
link_path "$canonical_home/resources/hooks" "$copilot_home/hooks"
link_path "$canonical_home/resources/prompts" "$vscode_user_dir/prompts"
write_managed_shell_env
merge_vscode_settings
write_state
cat <<EOF
Bootstrap complete.
Canonical home: $canonical_home
Repository root: $repo_root
Copilot home: $copilot_home
VS Code user dir: $vscode_user_dir
Linked default discovery paths back to the repository for skills, agents,
instructions, hooks, and prompts.
Merged managed VS Code settings into:
$vscode_settings_file
Installed managed Copilot CLI shell environment into:
$managed_shell_env
Linked shell startup file:
$shell_rc_file
Optional VS Code feature flags are available in:
$canonical_home/config/vscode/settings.template.jsonc
Optional Copilot CLI environment template is available in:
$canonical_home/config/copilot-cli/env.example.sh
EOF
}
main "$@"