Something went wrong. Try again.
Dotfiles for my macos setup
Something went wrong. Try again.
11 kB · 364 lines
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365#!/usr/bin/env bashset -euo pipefailtemp_files=()cleanup() { rm -f "${temp_files[@]}"}trap cleanup EXIT# Create a temp file that ends in .md so editors detect markdown.new_md_tempfile() { local f f=$(mktemp -t pr-description.XXXXXX) mv "$f" "$f.md" printf '%s.md' "$f"}usage() { cat <<'EOF'Usage: pr-create [options]All options are optional. Anything not provided is asked for interactively(unless --yes is set, in which case sensible defaults are used).Options: --title <text> PR title (must start with a conventional type, e.g. "feat: ...", "fix(api): ...") --base <branch> Base branch ("" = repo default) --draft / --no-draft Create as draft PR --reviewers <default|cns> Reviewer set --generate-description Generate PR body with pi --no-generate-description Skip body generation --body-file <path> Use this file as the PR body (skips generation) --linear <"Closes ABC-1"> Add a Linear link line (repeatable) --edit / --no-edit Open body in $EDITOR before creating --push / --no-push Auto-confirm pushing the branch if needed -y, --yes Non-interactive: defaults for anything unspecified -h, --help Show this helpEOF}# Defaults for --yes mode.title=""base=""draft="" # "yes" | "no" | ""reviewers="" # "default" | "cns" | ""gen_desc="" # "yes" | "no" | ""body_file=""edit_desc="" # "yes" | "no" | ""push_choice="" # "yes" | "no" | ""linear_links=()assume_yes=0while [[ $# -gt 0 ]]; do case "$1" in --title) title=$2 shift 2 ;; --base) base=$2 shift 2 ;; --draft) draft="yes" shift ;; --no-draft) draft="no" shift ;; --reviewers) reviewers=$(printf '%s' "$2" | tr '[:upper:]' '[:lower:]') shift 2 ;; --generate-description) gen_desc="yes" shift ;; --no-generate-description) gen_desc="no" shift ;; --body-file) body_file=$2 gen_desc="no" shift 2 ;; --linear) linear_links+=("$2") shift 2 ;; --edit) edit_desc="yes" shift ;; --no-edit) edit_desc="no" shift ;; --push) push_choice="yes" shift ;; --no-push) push_choice="no" shift ;; -y | --yes) assume_yes=1 shift ;; -h | --help) usage exit 0 ;; *) echo "Unknown option: $1" >&2 usage >&2 exit 2 ;; esacdoneprompt_input() { local prompt=$1 if [[ $assume_yes -eq 1 ]]; then printf '' return fi if command -v gum >/dev/null 2>&1; then gum input --prompt "$prompt: " else local value read -r -p "$prompt: " value printf '%s\n' "$value" fi}prompt_menu() { local title=$1 shift if [[ $assume_yes -eq 1 ]]; then printf '%s\n' "$1" return fi if command -v gum >/dev/null 2>&1; then gum choose --header "$title" "$@" else local choice select choice in "$@"; do [[ -n "$choice" ]] && printf '%s\n' "$choice" && break done fi}# Normalize a yes/no value (case-insensitive) to "Yes"/"No"; empty stays empty.normalize_yn() { case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" in yes | y | true | 1) printf 'Yes' ;; no | n | false | 0) printf 'No' ;; *) printf '' ;; esac}generate_pr_description() { local body_file=$1 local prompt_file prompt_file=$(mktemp) temp_files+=("$prompt_file") cat >"$prompt_file" <<'EOF'Use the humanizer skill's audit loop internally to self-check the prose, then return ONLY the final result. Do not output the draft, the "still-AI" bullets, or any summary of changes — output only the final PR body, nothing else.Generate a short GitHub pull request description for the current branch.Requirements:- Concise and natural.- Summarize actual code/config changes in this PR.- Start directly with the bullets. No summary or intro sentence; the title is a separate field.- Prefer 2-4 bullets.- Do not invent details.- Do not include Linear ticket lines.- Output only the final PR description body.EOF local prompt prompt=$(cat "$prompt_file") local models=(anthropic-extra/claude-opus-4-8 anthropic/claude-opus-4-8) local model for model in "${models[@]}"; do if command -v gum >/dev/null 2>&1 && [[ $assume_yes -eq 0 ]]; then gum spin --title "Generating PR description with pi ($model)..." -- pi -p --no-session --model "$model" --thinking off "$prompt" >"$body_file" else echo "Generating PR description with pi ($model)..." >&2 pi -p --no-session --model "$model" --thinking off "$prompt" >"$body_file" fi [[ -s $body_file ]] && return 0 done return 1}edit_pr_description() { local body_file=$1 local editor=${EDITOR:-vi} "$editor" "$body_file"}ensure_branch_pushed() { local choice if ! git rev-parse --verify --quiet '@{u}' >/dev/null; then choice=$(normalize_yn "$push_choice") [[ -z "$choice" ]] && choice=$(prompt_menu "Current branch has no upstream. Push branch now?" "Yes" "No") if [[ "$choice" != "Yes" ]]; then echo "Current branch has no upstream. Push branch first." >&2 exit 1 fi git push --set-upstream origin "$(git branch --show-current)" return fi local unpushed unpushed=$(git rev-list --count '@{u}..HEAD') if [[ "$unpushed" -ne 0 ]]; then choice=$(normalize_yn "$push_choice") [[ -z "$choice" ]] && choice=$(prompt_menu "Current branch has $unpushed unpushed commit(s). Push branch now?" "Yes" "No") if [[ "$choice" != "Yes" ]]; then echo "Current branch has $unpushed unpushed commit(s). Push branch first." >&2 exit 1 fi git push fi}ensure_branch_pushed# Conventional commit types allowed in PR titles.allowed_types=(feat fix docs style refactor perf test build ci chore revert deps)validate_title() { local t=$1 # Match: type(optional-scope)!?: subject local re='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|deps)(\([^)]+\))?!?: .+' [[ "$t" =~ $re ]]}title_hint="format: <type>[(scope)]: <subject> | types: ${allowed_types[*]}"if [[ -n "$title" ]] && ! validate_title "$title"; then echo "Invalid PR title: '$title'" >&2 echo "$title_hint" >&2 exit 1fiwhile [[ -z "$title" ]]; do title=$(prompt_input "Pull Request Title ($title_hint)") if [[ -z "$title" ]]; then echo "PR title is required." >&2 [[ $assume_yes -eq 1 ]] && exit 1 continue fi if ! validate_title "$title"; then echo "Invalid title. $title_hint" >&2 [[ $assume_yes -eq 1 ]] && exit 1 title="" fidone# Only prompt for base if user did not pass --base at all (empty is a valid explicit value when --yes).if [[ -z "$base" && $assume_yes -eq 0 ]]; then # Allow an empty answer to mean "default branch". base=$(prompt_input "Base Branch (leave empty for default branch)")fidraft_norm=$(normalize_yn "$draft")[[ -z "$draft_norm" ]] && draft_norm=$(prompt_menu "Create as draft PR?" "No" "Yes")if [[ -z "$reviewers" ]]; then reviewer_choice=$(prompt_menu "Add Reviewers" "Default" "CNS")else case "$reviewers" in default) reviewer_choice="Default" ;; cns) reviewer_choice="CNS" ;; *) echo "Unknown --reviewers value: $reviewers (expected default|cns)" >&2 exit 2 ;; esacfiif [[ -z "$body_file" ]]; then gen_norm=$(normalize_yn "$gen_desc") [[ -z "$gen_norm" ]] && gen_norm=$(prompt_menu "Generate PR description with pi?" "No" "Yes") if [[ "$gen_norm" == "Yes" ]]; then body_file=$(new_md_tempfile) temp_files+=("$body_file") generate_pr_description "$body_file" fifi# Interactive Linear collection only when no --linear flags were passed and not --yes.if [[ ${#linear_links[@]} -eq 0 && $assume_yes -eq 0 ]]; then add_linear=$(prompt_menu "Any Linear ticket(s) linked to this PR?" "No" "Yes") while [[ "$add_linear" == "Yes" ]]; do linear_ticket=$(prompt_input "Linear ticket ID (example: ABC-123)") linear_relation=$(prompt_menu "Relationship for $linear_ticket" "Closes" "Ref") if [[ -n "$linear_ticket" ]]; then linear_links+=("$linear_relation $linear_ticket") fi add_linear=$(prompt_menu "Add another Linear ticket?" "No" "Yes") donefilinear_appended=0append_linear_links() { { printf '\n\n' printf '%s\n' "${linear_links[@]}" } >>"$1" linear_appended=1}edit_norm=$(normalize_yn "$edit_desc")[[ -z "$edit_norm" ]] && edit_norm=$(prompt_menu "Open PR description in \$EDITOR before creating PR?" "No" "Yes")if [[ "$edit_norm" == "Yes" ]]; then if [[ -z "$body_file" ]]; then body_file=$(new_md_tempfile) temp_files+=("$body_file") fi # Append Linear links before editing so they are part of the description the # user customizes (and are not lost when a fresh body file is edited). [[ ${#linear_links[@]} -gt 0 ]] && append_linear_links "$body_file" edit_pr_description "$body_file"fi# Append Linear links if we have a body file and haven't done so already.if [[ $linear_appended -eq 0 && -n "$body_file" && ${#linear_links[@]} -gt 0 ]]; then append_linear_links "$body_file"fiargs=(pr create --assignee @me --title "$title" --fill)[[ -n "$body_file" ]] && args+=(--body-file "$body_file")[[ -n "$base" ]] && args+=(--base "$base")[[ "$draft_norm" == "Yes" ]] && args+=(--draft)[[ "$reviewer_choice" == "CNS" ]] && args+=(--reviewer "peterbornerup,farhadh,jdejnek,finoutlook,befe-oh-no")pr_url=$(gh "${args[@]}")printf '%s\n' "$pr_url"# If we created the PR without a body but have Linear links, append them after creation.if [[ -z "$body_file" && ${#linear_links[@]} -gt 0 ]]; then edit_body_file=$(new_md_tempfile) temp_files+=("$edit_body_file") gh pr view "$pr_url" --json body --jq '.body' >"$edit_body_file" { printf '\n\n' printf '%s\n' "${linear_links[@]}" } >>"$edit_body_file" gh pr edit "$pr_url" --body-file "$edit_body_file" >/dev/null echo "Added Linear links to PR description." >&2fi