#!/usr/bin/env bash # Interactive version bump. # # Shows the current version, asks for the new version (with a sensible default: # patch-level bump), and asks for release notes. Then: # 1. Creates startos/versions/v.ts # 2. Updates startos/versions/index.ts (adds import, updates `current:`, # pushes old current into `other:`) # # If the file .release-notes-pending.txt exists in the project root, its # contents are shown as the default release notes (just press Enter to accept). # This is the convention Claude uses after making code changes. The file is # deleted on a successful bump. # # Flags: # --from-deploy Treat the absence of .release-notes-pending.txt as the # signal that no new work needs to ship — exit 0 without # bumping. Lets `make deploy` always include this step # without forcing a redundant bump when the user (or a # prior run) already bumped. # # Run standalone with `make bump`, or as the first step of `make deploy`. set -euo pipefail FROM_DEPLOY=0 for arg in "$@"; do case "$arg" in --from-deploy) FROM_DEPLOY=1 ;; esac done SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" VERSIONS_DIR="$PROJECT_ROOT/startos/versions" INDEX_FILE="$VERSIONS_DIR/index.ts" PENDING_NOTES_FILE="$PROJECT_ROOT/.release-notes-pending.txt" # When invoked from `make deploy`, treat a missing pending-notes file as # "nothing new to ship — current version is already fresh, skip the bump." # Standalone `make bump` always prompts. if [ "$FROM_DEPLOY" = "1" ] && [ ! -f "$PENDING_NOTES_FILE" ]; then echo "" echo " (No .release-notes-pending.txt — current version is already bumped. Skipping.)" echo "" exit 0 fi # --- Discover current version --- CURRENT_VAR="$(sed -nE "s/.*current:[[:space:]]*(v_[0-9_]+).*/\1/p" "$INDEX_FILE" | head -1)" if [ -z "$CURRENT_VAR" ]; then echo "X Could not find current version in $INDEX_FILE" >&2 exit 1 fi CURRENT_DOT="$(echo "$CURRENT_VAR" | sed 's/^v_//; s/_/./g')" IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_DOT" SUGGESTED="$MAJOR.$MINOR.$((PATCH + 1))" echo "" echo "═══════════════════════════════════════════" echo " Bumping version" echo "═══════════════════════════════════════════" echo "" echo " Current: $CURRENT_DOT" echo "" # --- Prompt for new version --- read -r -p " New version [$SUGGESTED]: " NEW_VERSION NEW_VERSION="${NEW_VERSION:-$SUGGESTED}" if ! [[ "$NEW_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "X Invalid version: '$NEW_VERSION' (expected format: x.y.z, e.g. 0.1.10)" >&2 exit 1 fi NEW_FILE="$VERSIONS_DIR/v${NEW_VERSION}.ts" if [ -f "$NEW_FILE" ]; then echo "X Version file already exists: $NEW_FILE" >&2 echo " (If you want to re-run, delete that file first.)" >&2 exit 1 fi NEW_VAR="v_$(echo "$NEW_VERSION" | tr '.' '_')" # --- Prompt for release notes --- # If Claude (or you) left suggested notes in .release-notes-pending.txt, show # them as the default. Press Enter to accept, or type something different. SUGGESTED_NOTES="" if [ -f "$PENDING_NOTES_FILE" ]; then # Read the file, trim leading/trailing whitespace, collapse interior newlines # into spaces so it fits the one-line release-notes format. SUGGESTED_NOTES="$(tr '\n' ' ' < "$PENDING_NOTES_FILE" | sed -e 's/[[:space:]]\{1,\}/ /g' -e 's/^ //' -e 's/ $//')" fi echo "" if [ -n "$SUGGESTED_NOTES" ]; then echo " Suggested release notes (from .release-notes-pending.txt):" echo " $SUGGESTED_NOTES" echo "" read -r -p " Release notes (Enter to accept, or type new): " RELEASE_NOTES RELEASE_NOTES="${RELEASE_NOTES:-$SUGGESTED_NOTES}" else read -r -p " Release notes (one-liner, what changed in $NEW_VERSION?): " RELEASE_NOTES fi if [ -z "$RELEASE_NOTES" ]; then echo "X Release notes are required" >&2 exit 1 fi # Escape for TypeScript single-quoted string: backslash first, then single quote ESCAPED_NOTES="$(printf '%s' "$RELEASE_NOTES" | sed -e 's/\\/\\\\/g' -e "s/'/\\\\'/g")" # Clean up partial state if anything below fails cleanup_on_error() { [ -f "$NEW_FILE" ] && rm -f "$NEW_FILE" [ -f "$INDEX_FILE.tmp" ] && rm -f "$INDEX_FILE.tmp" [ -f "$INDEX_FILE.bak" ] && rm -f "$INDEX_FILE.bak" } trap cleanup_on_error ERR # --- 1. Create new version file --- cat > "$NEW_FILE" < {}, down: async ({ effects }) => {}, }, }) EOF # --- 2. Update index.ts --- # Insert `import { v_NEW } from './vNEW'` right after the last existing version # import, so the imports stay contiguous. NEW_IMPORT="import { $NEW_VAR } from './v$NEW_VERSION'" awk -v new_import="$NEW_IMPORT" ' /^import \{ v_[0-9_]+ \} from/ { last_imp = NR } { lines[NR] = $0 } END { for (i = 1; i <= NR; i++) { print lines[i] if (i == last_imp) print new_import } } ' "$INDEX_FILE" > "$INDEX_FILE.tmp" mv "$INDEX_FILE.tmp" "$INDEX_FILE" # Point `current:` at the new version, and prepend the old current into `other:`. # Use -i.bak for macOS/BSD + Linux/GNU sed compatibility. sed -i.bak \ -e "s/current: $CURRENT_VAR/current: $NEW_VAR/" \ -e "s/other: \[/other: [$CURRENT_VAR, /" \ "$INDEX_FILE" rm -f "$INDEX_FILE.bak" trap - ERR # --- Clean up the pending-notes scratch file now that we've consumed it --- if [ -f "$PENDING_NOTES_FILE" ]; then rm -f "$PENDING_NOTES_FILE" fi echo "" echo " ✓ Created startos/versions/v${NEW_VERSION}.ts" echo " ✓ Wired up in startos/versions/index.ts" echo " ✓ Version: $CURRENT_DOT → $NEW_VERSION" echo ""