aboutsummaryrefslogtreecommitdiffstats
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/release.sh508
-rwxr-xr-xscripts/span-table.ts420
2 files changed, 0 insertions, 928 deletions
diff --git a/scripts/release.sh b/scripts/release.sh
deleted file mode 100755
index 8e0717a2..00000000
--- a/scripts/release.sh
+++ /dev/null
@@ -1,508 +0,0 @@
-#!/usr/bin/env bash
-#
-# Atuin CLI Release Script
-#
-# But first โ€” make a cup of tea. Releases without tea are a crime. ๐Ÿซ–
-#
-
-set -euo pipefail
-
-WORKDIR=""
-
-cleanup_on_error() {
- if [[ $? -ne 0 && -n "$WORKDIR" ]]; then
- echo ""
- echo -e " \033[0;31mโœ—\033[0m Script failed. Working directory preserved at:"
- echo -e " \033[2m$WORKDIR\033[0m"
- fi
-}
-trap cleanup_on_error EXIT
-
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-# Formatting
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-CYAN='\033[0;36m'
-MAGENTA='\033[0;35m'
-BOLD='\033[1m'
-DIM='\033[2m'
-NC='\033[0m'
-
-info() { echo -e " ${BLUE}โ–ธ${NC} $*"; }
-success() { echo -e " ${GREEN}โœ“${NC} $*"; }
-warn() { echo -e " ${YELLOW}โš ${NC} $*"; }
-die() { echo -e " ${RED}โœ—${NC} $*" >&2; exit 1; }
-
-step() {
- echo ""
- echo -e " ${MAGENTA}${BOLD}โ”โ”โ” $* โ”โ”โ”${NC}"
- echo ""
-}
-
-confirm() {
- echo -en " ${CYAN}โ–ธ${NC} $1 ${DIM}[y/N]${NC} "
- read -r reply
- [[ "$reply" =~ ^[Yy]$ ]]
-}
-
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-# Banner
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-
-banner() {
- echo ""
- echo -e "${CYAN}"
- cat <<'BANNER'
-
- :
- =#*#=
- #*+*#.
- #**++*+
- =#*++++#.
- **+++++**
- :#++++++*#.
- **+++++++*#
- -#*+++++++*#-
- ##*****#####*
- -*##########:.+####*.
- ***********####*****+
- :**#############*#+
- #+----------------*-
- -*###########**+++##-----=##=-----=##=#
- .#*-==.. =*------------------*
- -#-::::::*=----------#*---------=----=--=*
- +*:::::::--:::::::::::**---+#=----===---=*
- #*::::::::=::::::::::::**-----+#*=-----+#-
- .#+::::::::=-::::::::::::**--------=*####
- #=::::::::=*:::::::::::::+*---------=#-*#=
- ##-:::-**+=+*-:::::::::::-#=--------=#=##-
- :#+**+========++=::::-=++=#+--------+#+#*
- +##++============+*#+=====*+--------+#*#
- *=*#*+==+**++++*#++*====+###--------*###*
- *=-*##=+#=-------#**===+#+---------=##*=#
- *=-=*#**+---------#*+==+#----------*##--#
- :#*- ###=---------#+*==+#=-------=###=-=#
- +=---------#+#+++#*++++*##*== .. #
- ...... +=--*++*+=-#####+-. -+****=.
- ....... =*== . + ...........
- ............::-:............................
- ............................................
- ....###....########.##.....##.####.##....##.
- ...##.##......##....##.....##..##..###...##.
- ..##...##.....##....##.....##..##..####..##.
- .##.....##....##....##.....##..##..##.##.##.
- .#########....##....##.....##..##..##..####.
- .##.....##....##....##.....##..##..##...###.
- .##.....##....##.....#######..####.##....##.
- ............................................
-
- ~ Magical Shell History ~
- Release Script
-
-BANNER
- echo -e "${NC}"
- echo ""
-}
-
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-# 1. Dependency check
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-
-check_deps() {
- step "Dependencies"
-
- local missing=()
- local deps=(git gsed cargo gh git-cliff)
-
- for cmd in "${deps[@]}"; do
- if command -v "$cmd" &>/dev/null; then
- success "${BOLD}$cmd${NC} ${DIM}$(command -v "$cmd")${NC}"
- else
- echo -e " ${RED}โœ—${NC} ${BOLD}$cmd${NC} not found"
- missing+=("$cmd")
- fi
- done
-
- if (( ${#missing[@]} )); then
- echo ""
- die "Missing required tools: ${missing[*]}\n Install with: ${DIM}brew install ${missing[*]}${NC}"
- fi
-}
-
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-# 2. Clone into working directory
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-
-setup_workdir() {
- step "Working Directory"
-
- WORKDIR=$(mktemp -d)
- info "Created ${DIM}$WORKDIR${NC}"
-
- info "Cloning atuinsh/atuin..."
- git clone --quiet git@github.com:atuinsh/atuin.git "$WORKDIR"
- cd "$WORKDIR"
- success "Repository cloned"
-}
-
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-# 3. Version
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-
-detect_current_version() {
- sed -n '/^\[workspace\.package\]/,/^\[/s/^version = "\(.*\)"/\1/p' Cargo.toml
-}
-
-get_version() {
- step "Version"
-
- CURRENT_VERSION=$(detect_current_version)
- info "Current version: ${BOLD}$CURRENT_VERSION${NC}"
-
- # Suggest the next version based on conventional commits
- local suggested
- suggested=$(git-cliff --bumped-version 2>/dev/null | sed 's/^v//' || true)
- if [[ -n "$suggested" && "$suggested" != "$CURRENT_VERSION" ]]; then
- info "Suggested next: ${BOLD}$suggested${NC} ${DIM}(based on conventional commits)${NC}"
- fi
-
- echo ""
-
- if [[ -n "${NEW_VERSION:-}" ]]; then
- info "Using version from environment: ${BOLD}$NEW_VERSION${NC}"
- else
- echo -en " ${CYAN}โ–ธ${NC} New version ${DIM}(without 'v' prefix)${NC}: "
- read -r NEW_VERSION
- fi
-
- [[ -n "$NEW_VERSION" ]] || die "Version cannot be empty"
-
- IS_PRERELEASE=false
- if [[ "$NEW_VERSION" == *-* ]]; then
- IS_PRERELEASE=true
- warn "Pre-release detected"
- fi
-
- echo ""
- info "${BOLD}$CURRENT_VERSION${NC} โ†’ ${BOLD}$NEW_VERSION${NC}"
- echo ""
- confirm "Proceed with release?" || { info "Aborted."; exit 0; }
-}
-
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-# 4. Update version numbers
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-
-update_versions() {
- step "Updating Versions"
-
- info "Creating release branch: ${BOLD}$NEW_VERSION${NC}"
- git checkout -b "$NEW_VERSION" --quiet
-
- local version_pattern="${CURRENT_VERSION//./\\.}"
-
- info "Replacing ${DIM}$CURRENT_VERSION${NC} โ†’ ${DIM}$NEW_VERSION${NC} in Cargo.toml files..."
- find . -type f -name 'Cargo.toml' -not -path './.git/*' \
- -exec gsed -i "s/$version_pattern/$NEW_VERSION/g" {} \;
-
- info "Running ${DIM}cargo check${NC} to update Cargo.lock (this may take a moment)..."
- cargo check --quiet 2>&1 || cargo check
-
- echo ""
- info "Changed files:"
- git diff --stat | sed 's/^/ /'
-
- echo ""
-
- # Verify the workspace version was updated
- local new_ws_version
- new_ws_version=$(detect_current_version)
- if [[ "$new_ws_version" == "$NEW_VERSION" ]]; then
- success "Workspace version updated"
- else
- die "Workspace version is '$new_ws_version', expected '$NEW_VERSION'"
- fi
-
- # Verify we didn't break anything unexpected โ€” show version-related
- # lines in the diff for review
- echo ""
- info "Version changes in diff:"
- git diff --unified=0 -- '*.toml' \
- | grep -E '^\+.*version' \
- | grep -v '^\+\+\+' \
- | sed 's/^/ /' || true
- echo ""
-
- confirm "Version changes look correct?" || { die "Aborting โ€” fix versions manually in $WORKDIR"; }
-}
-
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-# 5. Changelog
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-
-update_changelog() {
- step "Changelog"
-
- # cliff.toml's ignore_tags already ignores beta/alpha tags, so
- # --unreleased always gives us everything since the last stable release.
- #
- # Prereleases: heading is ## [unreleased] (running tally)
- # Stable: heading is ## X.Y.Z (versioned entry)
- local cliff_args=(--unreleased --strip all)
-
- if $IS_PRERELEASE; then
- info "Updating ${BOLD}Unreleased${NC} section..."
- else
- cliff_args+=(--tag "v$NEW_VERSION")
- info "Generating changelog for ${BOLD}$NEW_VERSION${NC}..."
- fi
-
- local new_entry
- new_entry=$(git-cliff "${cliff_args[@]}" 2>/dev/null || true)
-
- # Check if the entry is empty (just a heading with no content)
- if [[ -z "$new_entry" ]] || [[ "$(echo "$new_entry" | grep -c '[a-zA-Z]')" -le 1 ]]; then
- warn "No unreleased changes detected by git-cliff"
- warn "You may want to add entries manually in the editor"
- if $IS_PRERELEASE; then
- new_entry="## [unreleased]"
- else
- new_entry="## $NEW_VERSION"
- fi
- else
- local commit_count
- commit_count=$(echo "$new_entry" | grep -c '^- ' || true)
- success "Generated entry with ${BOLD}$commit_count${NC} item(s)"
- fi
-
- # Remove any existing [unreleased] section โ€” we'll replace it with
- # either an updated unreleased section or a versioned one
- if grep -qi '^\## \[unreleased\]' CHANGELOG.md; then
- info "Removing old Unreleased section..."
- awk '
- /^## \[[Uu]nreleased\]/ { skip=1; next }
- /^## / { skip=0 }
- !skip
- ' CHANGELOG.md > CHANGELOG.md.tmp
- mv CHANGELOG.md.tmp CHANGELOG.md
- fi
-
- # Insert the new entry before the first existing version heading
- local insert_line
- insert_line=$(grep -n '^## ' CHANGELOG.md | head -1 | cut -d: -f1)
-
- if [[ -n "$insert_line" ]]; then
- {
- head -n "$((insert_line - 1))" CHANGELOG.md
- echo "$new_entry"
- echo ""
- echo ""
- tail -n "+$insert_line" CHANGELOG.md
- } > CHANGELOG.md.tmp
- mv CHANGELOG.md.tmp CHANGELOG.md
- else
- warn "No existing version headings found โ€” appending to end"
- echo "" >> CHANGELOG.md
- echo "$new_entry" >> CHANGELOG.md
- fi
-
- echo ""
- info "Opening CHANGELOG.md in your editor for review..."
- info "${DIM}Verify the entry, make any edits, then save and close.${NC}"
- echo ""
- echo -en " ${DIM}Press Enter to open editor...${NC}"
- read -r
- "${EDITOR:-${VISUAL:-vi}}" CHANGELOG.md
- success "Changelog finalized"
-}
-
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-# 6. Commit and push
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-
-commit_and_push() {
- step "Commit & Push"
-
- git add .
- git commit --quiet -m "chore(release): prepare for release $NEW_VERSION"
- success "Committed"
-
- info "Pushing branch..."
- git push --quiet --set-upstream origin "$(git branch --show-current)" 2>&1
- success "Pushed to origin/${BOLD}$NEW_VERSION${NC}"
-}
-
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-# 7. Pull request
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-
-extract_changelog_entry() {
- # Extract the changelog body (without the heading) for the entry we just wrote.
- # Prereleases use ## [unreleased], stable uses ## X.Y.Z
- local heading
- if $IS_PRERELEASE; then
- heading='## \[unreleased\]'
- else
- heading="## ${NEW_VERSION//./\\.}"
- fi
- awk "/^${heading}/{found=1; next} /^## /{if(found) exit} found" CHANGELOG.md
-}
-
-create_pr() {
- step "Pull Request"
-
- local changelog_body
- changelog_body=$(extract_changelog_entry)
-
- local pr_body
- pr_body="Release preparation for v${NEW_VERSION}."
- if [[ -n "$changelog_body" ]]; then
- pr_body="$(cat <<EOF
-Release preparation for v${NEW_VERSION}.
-
-## Changelog
-
-$changelog_body
-EOF
-)"
- fi
-
- info "Creating PR..."
- PR_URL=$(gh pr create \
- --title "chore(release): prepare for release $NEW_VERSION" \
- --body "$pr_body" \
- --repo atuinsh/atuin)
-
- success "PR created: ${BOLD}$PR_URL${NC}"
-
- local pr_number
- pr_number=$(echo "$PR_URL" | grep -o '[0-9]*$')
-
- echo ""
- info "Waiting for PR #${BOLD}$pr_number${NC} to be merged..."
- info "${DIM}Review and merge the PR โ€” this script will detect it automatically.${NC}"
- echo ""
-
- while true; do
- local state
- state=$(gh pr view "$pr_number" --repo atuinsh/atuin --json state --jq '.state' 2>/dev/null || echo "UNKNOWN")
-
- case "$state" in
- MERGED)
- echo ""
- success "PR #$pr_number merged!"
- break
- ;;
- CLOSED)
- echo ""
- die "PR #$pr_number was closed without merging"
- ;;
- *)
- printf "\r ${DIM}โณ PR #%s is %s โ€” checking again in 5s...${NC} " "$pr_number" "$state"
- sleep 5
- ;;
- esac
- done
-}
-
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-# 8. Tag
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-
-tag_release() {
- step "Tag & Release"
-
- info "Switching to main and pulling..."
- git checkout main --quiet
- git pull --quiet
-
- info "Creating tag ${BOLD}v$NEW_VERSION${NC}"
- git tag "v$NEW_VERSION"
-
- info "Pushing tag..."
- git push --tags
- success "Tag ${BOLD}v$NEW_VERSION${NC} pushed โ€” release workflow triggered"
-}
-
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-# 9. Publish to crates.io
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-
-publish_crates() {
- step "Publish to crates.io"
-
- if $IS_PRERELEASE; then
- warn "Pre-release โ€” skipping crates.io publish"
- return
- fi
-
- if ! confirm "Publish to crates.io?"; then
- info "Skipping"
- return
- fi
-
- local crates=(
- atuin-common
- atuin-client
- atuin-ai
- atuin-dotfiles
- atuin-history
- atuin-nucleo/matcher
- atuin-nucleo
- atuin-daemon
- atuin-kv
- atuin-scripts
- atuin-server-database
- atuin-server-postgres
- atuin-server-sqlite
- atuin-server
- atuin-pty-proxy
- atuin
- )
-
- for crate in "${crates[@]}"; do
- info "Publishing ${BOLD}$crate${NC}..."
- local output
- # --no-verify: skip rebuild during publish โ€” the code already passed
- # CI, and verification fails on workspace crates whose freshly-published
- # dependencies haven't been indexed by crates.io yet
- if output=$(cd "crates/$crate" && cargo publish --no-verify 2>&1); then
- success "$crate published"
- elif echo "$output" | grep -q "already uploaded"; then
- warn "$crate already published, skipping"
- else
- echo "$output" >&2
- die "Failed to publish $crate"
- fi
- done
-}
-
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-# Main
-# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
-
-main() {
- banner
- check_deps
- setup_workdir
- get_version
- update_versions
- update_changelog
- commit_and_push
- create_pr
- tag_release
- publish_crates
-
- step "Done ๐ŸŽ‰"
- success "Released ${BOLD}v$NEW_VERSION${NC}"
- echo ""
- info "Working directory: ${DIM}$WORKDIR${NC}"
- info "Clean up with: ${DIM}rm -rf $WORKDIR${NC}"
- echo ""
-}
-
-main "$@"
diff --git a/scripts/span-table.ts b/scripts/span-table.ts
deleted file mode 100755
index 3656f129..00000000
--- a/scripts/span-table.ts
+++ /dev/null
@@ -1,420 +0,0 @@
-#!/usr/bin/env bun
-/**
- * Analyze span timing JSON logs generated with ATUIN_SPAN
- *
- * Usage: bun scripts/span-table.ts <file.json> [options]
- * --filter <pattern> Only show spans matching pattern (regex)
- * --sort <field> Sort by: calls, avg, total, p99 (default: total)
- * --top <n> Show top N spans (default: 20)
- * --detail <span> Show individual calls for a specific span
- * --all Include internal/library spans
- */
-
-import { readFileSync } from "fs";
-
-interface SpanEvent {
- timestamp: string;
- level: string;
- fields: {
- message: string;
- "time.busy"?: string;
- "time.idle"?: string;
- };
- target: string;
- span?: {
- name: string;
- [key: string]: unknown;
- };
- spans?: Array<{ name: string; [key: string]: unknown }>;
-}
-
-interface SpanStats {
- name: string;
- calls: number;
- busyTimes: number[]; // in microseconds
- idleTimes: number[];
- parentCounts: Map<string, number>; // parent span name -> count
-}
-
-// Parse duration strings like "1.23ms", "456ยตs", "789ns" to microseconds
-function parseDuration(duration: string): number {
- const match = duration.match(/^([\d.]+)(ns|ยตs|us|ms|s)$/);
- if (!match) return 0;
-
- const value = parseFloat(match[1]);
- const unit = match[2];
-
- switch (unit) {
- case "ns":
- return value / 1000;
- case "ยตs":
- case "us":
- return value;
- case "ms":
- return value * 1000;
- case "s":
- return value * 1_000_000;
- default:
- return 0;
- }
-}
-
-// Format microseconds for display
-function formatDuration(us: number): string {
- if (us < 1) {
- return `${(us * 1000).toFixed(0)}ns`;
- } else if (us < 1000) {
- return `${us.toFixed(2)}ยตs`;
- } else if (us < 1_000_000) {
- return `${(us / 1000).toFixed(2)}ms`;
- } else {
- return `${(us / 1_000_000).toFixed(2)}s`;
- }
-}
-
-function percentile(arr: number[], p: number): number {
- if (arr.length === 0) return 0;
- const sorted = [...arr].sort((a, b) => a - b);
- const idx = Math.floor(sorted.length * p);
- return sorted[Math.min(idx, sorted.length - 1)];
-}
-
-function parseJsonLines(content: string): SpanEvent[] {
- const events: SpanEvent[] = [];
- for (const line of content.trim().split("\n")) {
- if (!line.trim()) continue;
- try {
- events.push(JSON.parse(line));
- } catch {
- // Skip malformed lines
- }
- }
- return events;
-}
-
-function main() {
- const args = process.argv.slice(2);
-
- // Parse arguments
- let filterPattern: RegExp | null = null;
- let sortField = "total";
- let topN = 20;
- let detailSpan: string | null = null;
- let showAll = false;
- const files: string[] = [];
-
- for (let i = 0; i < args.length; i++) {
- if (args[i] === "--filter" && args[i + 1]) {
- filterPattern = new RegExp(args[++i]);
- } else if (args[i] === "--sort" && args[i + 1]) {
- sortField = args[++i];
- } else if (args[i] === "--top" && args[i + 1]) {
- topN = parseInt(args[++i], 10);
- } else if (args[i] === "--detail" && args[i + 1]) {
- detailSpan = args[++i];
- } else if (args[i] === "--all") {
- showAll = true;
- } else if (!args[i].startsWith("-")) {
- files.push(args[i]);
- }
- }
-
- if (files.length === 0) {
- console.error("Usage: bun scripts/span-table.ts <file.json> [options]");
- console.error(" --filter <pattern> Only show spans matching pattern (regex)");
- console.error(" --sort <field> Sort by: calls, avg, total, p99 (default: total)");
- console.error(" --top <n> Show top N spans (default: 20)");
- console.error(" --detail <span> Show individual calls for a specific span");
- console.error(" --all Include internal/library spans");
- process.exit(1);
- }
-
- // Parse all files
- const allEvents: SpanEvent[] = [];
- for (const file of files) {
- const content = readFileSync(file, "utf-8");
- for (const event of parseJsonLines(content)) {
- allEvents.push(event);
- }
- }
-
- // Filter to close events and aggregate by span name
- const spans = new Map<string, SpanStats>();
-
- for (const event of allEvents) {
- if (event.fields?.message !== "close") continue;
- if (!event.span?.name) continue;
- if (!event.fields["time.busy"]) continue;
-
- const name = event.span.name;
-
- // Apply filter if specified
- if (filterPattern && !filterPattern.test(name)) continue;
-
- // Skip noisy internal spans unless explicitly requested
- if (
- !showAll &&
- !filterPattern &&
- !detailSpan &&
- (name.startsWith("FramedRead::") ||
- name.startsWith("FramedWrite::") ||
- name.startsWith("Prioritize::") ||
- name === "poll" ||
- name === "poll_ready" ||
- name === "Connection" ||
- name.startsWith("assign_") ||
- name.startsWith("reserve_") ||
- name.startsWith("try_") ||
- name.startsWith("send_") ||
- name.startsWith("pop_"))
- ) {
- continue;
- }
-
- if (!spans.has(name)) {
- spans.set(name, { name, calls: 0, busyTimes: [], idleTimes: [], parentCounts: new Map() });
- }
-
- const stats = spans.get(name)!;
- stats.calls++;
- stats.busyTimes.push(parseDuration(event.fields["time.busy"]));
- if (event.fields["time.idle"]) {
- stats.idleTimes.push(parseDuration(event.fields["time.idle"]));
- }
-
- // Track parent relationship (immediate parent is the last element in spans array)
- const parents = event.spans || [];
- const parentName = parents.length > 0 ? parents[parents.length - 1].name : "__root__";
- stats.parentCounts.set(parentName, (stats.parentCounts.get(parentName) || 0) + 1);
- }
-
- if (spans.size === 0) {
- console.error("No matching span close events found");
- process.exit(1);
- }
-
- // Detail mode: show individual calls for a specific span
- if (detailSpan) {
- const detailEvents: Array<{
- timestamp: string;
- busy: number;
- idle: number;
- fields: Record<string, unknown>;
- parents: string[];
- }> = [];
-
- for (const event of allEvents) {
- if (event.fields?.message !== "close") continue;
- if (event.span?.name !== detailSpan) continue;
- if (!event.fields["time.busy"]) continue;
-
- // Extract span fields (excluding name)
- const fields: Record<string, unknown> = {};
- if (event.span) {
- for (const [k, v] of Object.entries(event.span)) {
- if (k !== "name") fields[k] = v;
- }
- }
-
- // Get parent span names
- const parents = (event.spans || []).map((s) => s.name);
-
- detailEvents.push({
- timestamp: event.timestamp,
- busy: parseDuration(event.fields["time.busy"]),
- idle: event.fields["time.idle"] ? parseDuration(event.fields["time.idle"]) : 0,
- fields,
- parents,
- });
- }
-
- if (detailEvents.length === 0) {
- console.error(`No events found for span "${detailSpan}"`);
- process.exit(1);
- }
-
- console.log("");
- console.log(`Individual calls for: ${detailSpan}`);
- console.log("-".repeat(110));
- console.log(
- "#".padStart(4) +
- "Wall".padStart(12) +
- "Busy".padStart(12) +
- "Idle".padStart(12) +
- " Fields"
- );
- console.log("-".repeat(110));
-
- detailEvents.forEach((e, i) => {
- const fieldsStr = Object.keys(e.fields).length > 0
- ? JSON.stringify(e.fields)
- : "";
-
- console.log(
- (i + 1).toString().padStart(4) +
- formatDuration(e.busy + e.idle).padStart(12) +
- formatDuration(e.busy).padStart(12) +
- formatDuration(e.idle).padStart(12) +
- " " +
- fieldsStr
- );
- });
-
- // Summary stats
- const busyTimes = detailEvents.map((e) => e.busy);
- const wallTimes = detailEvents.map((e) => e.busy + e.idle);
- console.log("");
- console.log(
- `Summary: ${detailEvents.length} calls\n` +
- ` Wall: avg=${formatDuration(wallTimes.reduce((a, b) => a + b, 0) / wallTimes.length)}, ` +
- `min=${formatDuration(Math.min(...wallTimes))}, ` +
- `max=${formatDuration(Math.max(...wallTimes))}, ` +
- `p50=${formatDuration(percentile(wallTimes, 0.5))}, ` +
- `p99=${formatDuration(percentile(wallTimes, 0.99))}\n` +
- ` Busy: avg=${formatDuration(busyTimes.reduce((a, b) => a + b, 0) / busyTimes.length)}, ` +
- `min=${formatDuration(Math.min(...busyTimes))}, ` +
- `max=${formatDuration(Math.max(...busyTimes))}, ` +
- `p50=${formatDuration(percentile(busyTimes, 0.5))}, ` +
- `p99=${formatDuration(percentile(busyTimes, 0.99))}`
- );
- return;
- }
-
- // Calculate stats
- const results = [...spans.values()].map((s) => {
- // Calculate wall times (busy + idle) for each call
- const wallTimes = s.busyTimes.map((busy, i) => busy + (s.idleTimes[i] || 0));
-
- // Find most common parent
- let mostCommonParent = "__root__";
- let maxCount = 0;
- for (const [parent, count] of s.parentCounts) {
- if (count > maxCount) {
- maxCount = count;
- mostCommonParent = parent;
- }
- }
-
- return {
- name: s.name,
- calls: s.calls,
- total: s.busyTimes.reduce((a, b) => a + b, 0),
- avg: s.busyTimes.reduce((a, b) => a + b, 0) / s.calls,
- min: Math.min(...s.busyTimes),
- max: Math.max(...s.busyTimes),
- p50: percentile(s.busyTimes, 0.5),
- p99: percentile(s.busyTimes, 0.99),
- avgWall: wallTimes.reduce((a, b) => a + b, 0) / s.calls,
- p50Wall: percentile(wallTimes, 0.5),
- p99Wall: percentile(wallTimes, 0.99),
- parent: mostCommonParent,
- };
- });
-
- // Build tree structure
- const childrenOf = new Map<string, string[]>();
- childrenOf.set("__root__", []);
- for (const r of results) {
- if (!childrenOf.has(r.name)) {
- childrenOf.set(r.name, []);
- }
- if (!childrenOf.has(r.parent)) {
- childrenOf.set(r.parent, []);
- }
- childrenOf.get(r.parent)!.push(r.name);
- }
-
- // Sort children by the specified field
- const resultMap = new Map(results.map(r => [r.name, r]));
- const sortChildren = (children: string[]) => {
- children.sort((a, b) => {
- const ra = resultMap.get(a);
- const rb = resultMap.get(b);
- if (!ra || !rb) return 0;
- switch (sortField) {
- case "calls":
- return rb.calls - ra.calls;
- case "avg":
- return rb.avg - ra.avg;
- case "p99":
- return rb.p99 - ra.p99;
- case "total":
- default:
- return rb.total - ra.total;
- }
- });
- };
-
- // Traverse tree to build ordered display list with depths
- const displayResults: Array<{ result: typeof results[0]; depth: number }> = [];
- const visited = new Set<string>();
-
- function traverse(name: string, depth: number) {
- if (visited.has(name)) return;
- visited.add(name);
-
- const result = resultMap.get(name);
- if (result) {
- displayResults.push({ result, depth });
- }
-
- const children = childrenOf.get(name) || [];
- sortChildren(children);
- for (const child of children) {
- traverse(child, depth + 1);
- }
- }
-
- // Start from roots
- const roots = childrenOf.get("__root__") || [];
- sortChildren(roots);
- for (const root of roots) {
- traverse(root, 0);
- }
-
- // Add any orphaned spans (whose parent wasn't in our span list)
- for (const r of results) {
- if (!visited.has(r.name)) {
- displayResults.push({ result: r, depth: 0 });
- }
- }
-
- // Apply topN limit
- const limitedResults = displayResults.slice(0, topN);
-
- console.log("");
- console.log(
- "Span Name".padEnd(40) +
- "Calls".padStart(6) +
- "Avg(wall)".padStart(11) +
- "P50(wall)".padStart(11) +
- "P99(wall)".padStart(11) +
- "Avg(busy)".padStart(11) +
- "P50(busy)".padStart(11) +
- "P99(busy)".padStart(11)
- );
- console.log("-".repeat(112));
-
- for (const { result: r, depth } of limitedResults) {
- const indent = " ".repeat(depth);
- const maxNameLen = 38 - indent.length;
- const truncatedName = r.name.length > maxNameLen ? "..." + r.name.slice(-(maxNameLen - 3)) : r.name;
- const displayName = indent + truncatedName;
-
- console.log(
- displayName.padEnd(40) +
- r.calls.toString().padStart(6) +
- formatDuration(r.avgWall).padStart(11) +
- formatDuration(r.p50Wall).padStart(11) +
- formatDuration(r.p99Wall).padStart(11) +
- formatDuration(r.avg).padStart(11) +
- formatDuration(r.p50).padStart(11) +
- formatDuration(r.p99).padStart(11)
- );
- }
-
- console.log("");
- console.log(`Showing ${limitedResults.length} of ${results.length} spans (sorted by ${sortField})`);
-}
-
-main();