324 lines
8.0 KiB
Bash
Executable File
324 lines
8.0 KiB
Bash
Executable File
#!/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=""
|
|
vscode_mcp_file=""
|
|
copilot_cli_mcp_file=""
|
|
local_mcp_overrides_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"
|
|
}
|
|
|
|
ensure_local_mcp_overrides() {
|
|
local example_file
|
|
|
|
local_mcp_overrides_file="$canonical_home/.local/mcp.local.jsonc"
|
|
example_file="$canonical_home/config/mcp/local-overrides.example.jsonc"
|
|
|
|
ensure_parent_dir "$local_mcp_overrides_file"
|
|
if [[ ! -f "$local_mcp_overrides_file" ]]; then
|
|
cp -- "$example_file" "$local_mcp_overrides_file"
|
|
fi
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
merge_managed_mcp_config() {
|
|
local node_bin
|
|
|
|
if ! node_bin="$(find_node_bin)"; then
|
|
printf 'Skipping managed MCP config merge because Node.js is not available.\n' >&2
|
|
return 0
|
|
fi
|
|
|
|
vscode_mcp_file="$vscode_user_dir/mcp.json"
|
|
copilot_cli_mcp_file="$copilot_home/mcp-config.json"
|
|
|
|
ensure_parent_dir "$vscode_mcp_file"
|
|
ensure_parent_dir "$copilot_cli_mcp_file"
|
|
|
|
"$node_bin" "$script_dir/merge-managed-mcp-config.mjs" \
|
|
--target "$vscode_mcp_file" \
|
|
--template "$canonical_home/config/mcp/vscode.mcp.template.jsonc" \
|
|
--server-key "servers" \
|
|
--overrides "$local_mcp_overrides_file" \
|
|
--set "COPILOT_RESOURCES_HOME=$canonical_home"
|
|
|
|
"$node_bin" "$script_dir/merge-managed-mcp-config.mjs" \
|
|
--target "$copilot_cli_mcp_file" \
|
|
--template "$canonical_home/config/mcp/copilot-cli.mcp.template.jsonc" \
|
|
--server-key "mcpServers" \
|
|
--overrides "$local_mcp_overrides_file" \
|
|
--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}",
|
|
"vscodeMcpFile": "${vscode_mcp_file}",
|
|
"copilotCliMcpFile": "${copilot_cli_mcp_file}",
|
|
"mcpLocalOverridesFile": "${local_mcp_overrides_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
|
|
ensure_local_mcp_overrides
|
|
merge_vscode_settings
|
|
merge_managed_mcp_config
|
|
|
|
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
|
|
|
|
Merged managed MCP configuration into:
|
|
$vscode_user_dir/mcp.json
|
|
$copilot_home/mcp-config.json
|
|
|
|
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
|
|
|
|
Machine-local MCP overrides live in:
|
|
$local_mcp_overrides_file
|
|
|
|
Optional Copilot CLI environment template is available in:
|
|
$canonical_home/config/copilot-cli/env.example.sh
|
|
EOF
|
|
}
|
|
|
|
main "$@"
|