#!/usr/bin/env bash # migrate-nbd-to-beans.sh — Migrate nbd tickets to beans # # Usage: bash scripts/migrate-nbd-to-beans.sh [project-dir] # Defaults to current directory if no arg given. # # Requirements: beans, jq set -euo pipefail PROJECT_DIR="${1:-.}" PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)" NBD_DIR="$PROJECT_DIR/.nbd/tickets" MAP_FILE="/tmp/nbd_id_map_$(echo "$PROJECT_DIR" | tr '/' '_').tsv" # ── Preflight ──────────────────────────────────────────────────────────────── if [[ ! -d "$NBD_DIR" ]]; then echo "ERROR: No .nbd/tickets directory found in $PROJECT_DIR" >&2 exit 1 fi command -v beans >/dev/null 2>&1 || { echo "ERROR: beans not found in PATH" >&2; exit 1; } command -v jq >/dev/null 2>&1 || { echo "ERROR: jq not found in PATH" >&2; exit 1; } # ── Field mappers ───────────────────────────────────────────────────────────── map_priority() { local p="${1:-5}" if [[ "$p" -ge 8 ]]; then echo "critical" elif [[ "$p" -eq 7 ]]; then echo "high" elif [[ "$p" -ge 5 ]]; then echo "normal" elif [[ "$p" -ge 3 ]]; then echo "low" else echo "deferred" fi } map_status() { case "$1" in todo) echo "todo" ;; in_progress) echo "in-progress" ;; done) echo "completed" ;; closed) echo "scrapped" ;; archived) echo "completed" ;; backlog) echo "draft" ;; *) echo "todo" ;; esac } map_type() { case "$1" in project) echo "epic" ;; feature) echo "feature" ;; task) echo "task" ;; bug) echo "bug" ;; *) echo "task" ;; esac } # ── Parse TOML frontmatter field (string value, strips surrounding quotes) ─── parse_field() { local field="$1" local file="$2" awk -v field="$field" ' /^\+\+\+$/ { fm_count++; if (fm_count == 2) exit; next } fm_count == 1 && $0 ~ "^" field " = " { line = $0 sub("^" field " = \"?", "", line) sub("\"?$", "", line) print line exit } ' "$file" } # ── Extract body (everything after second +++) ──────────────────────────────── extract_body() { local file="$1" awk '/^\+\+\+$/ { count++; if (count == 2) { body=1; next } } body { print }' "$file" } # ── Extract dep IDs from a TOML array string ───────────────────────────────── # Input: dependencies = ["abc123", "def456"] # Output: abc123\ndef456 (one per line) extract_dep_ids() { echo "$1" | grep -oE '[0-9a-f]{6}' || true } # ── Init ────────────────────────────────────────────────────────────────────── echo "==> Initializing beans in $PROJECT_DIR" (cd "$PROJECT_DIR" && beans init) # Clear map file: columns are old_id new_id deps_raw > "$MAP_FILE" tickets_created=0 tickets_failed=0 deps_wired=0 warnings=() # ── Pass 1: Create beans ────────────────────────────────────────────────────── echo "" echo "==> Pass 1: Creating beans from nbd tickets" body_tmp="$(mktemp)" trap 'rm -f "$body_tmp"' EXIT for ticket_file in "$NBD_DIR"/*.md; do [[ -f "$ticket_file" ]] || continue old_id="$(basename "$ticket_file" .md)" # Parse frontmatter fields title="$(parse_field title "$ticket_file")" priority_num="$(parse_field priority "$ticket_file")" status_raw="$(parse_field status "$ticket_file")" type_raw="$(parse_field ticket_type "$ticket_file")" deps_line="$(awk '/^\+\+\+$/ { fm_count++; if (fm_count == 2) exit; next } fm_count == 1 && /^dependencies = / { print; exit }' "$ticket_file")" # Map to beans values beans_priority="$(map_priority "${priority_num:-5}")" beans_status="$(map_status "${status_raw:-todo}")" beans_type="$(map_type "${type_raw:-task}")" # Write body to temp file to avoid quoting issues extract_body "$ticket_file" > "$body_tmp" # Create the bean result="$(cd "$PROJECT_DIR" && beans create \ --json \ --type "$beans_type" \ --status "$beans_status" \ --priority "$beans_priority" \ --body-file "$body_tmp" \ "$title" 2>&1)" || true if new_id="$(echo "$result" | jq -r '.bean.id // .id' 2>/dev/null)" && [[ -n "$new_id" && "$new_id" != "null" ]]; then printf '%s\t%s\t%s\n' "$old_id" "$new_id" "$deps_line" >> "$MAP_FILE" (( tickets_created++ )) || true echo " ✓ $old_id -> $new_id [$beans_status/$beans_priority/$beans_type] $title" else (( tickets_failed++ )) || true warnings+=("WARN: failed to create ticket for nbd/$old_id: $title") echo " ✗ $old_id FAILED: $title" echo " Response: $result" fi done # ── Pass 2: Wire dependencies ───────────────────────────────────────────────── echo "" echo "==> Pass 2: Wiring dependencies" while IFS=$'\t' read -r old_id new_id deps_line; do dep_ids="$(extract_dep_ids "$deps_line")" [[ -z "$dep_ids" ]] && continue while IFS= read -r dep_old_id; do [[ -z "$dep_old_id" ]] && continue # Look up the new beans ID for this dependency dep_new_id="$(awk -F'\t' -v id="$dep_old_id" '$1 == id { print $2 }' "$MAP_FILE")" if [[ -z "$dep_new_id" ]]; then warnings+=("WARN: dep $dep_old_id (for $old_id -> $new_id) not found in map — skipped") echo " ! dep $dep_old_id not found in map (for $new_id)" continue fi result="$(cd "$PROJECT_DIR" && beans update "$new_id" --blocked-by "$dep_new_id" --json 2>&1)" || true if echo "$result" | jq -e '.bean.id // .id' > /dev/null 2>&1; then (( deps_wired++ )) || true echo " ✓ $new_id blocked-by $dep_new_id (was $old_id blocked-by $dep_old_id)" else warnings+=("WARN: failed to wire dep $dep_new_id -> $new_id: $result") echo " ✗ failed to wire $dep_new_id -> $new_id" fi done <<< "$dep_ids" done < "$MAP_FILE" # ── Cleanup ─────────────────────────────────────────────────────────────────── echo "" echo "==> Removing .nbd directory" rm -rf "$PROJECT_DIR/.nbd" echo " Removed $PROJECT_DIR/.nbd" # ── Summary ─────────────────────────────────────────────────────────────────── echo "" echo "==========================================" echo " Migration complete for: $PROJECT_DIR" echo " Tickets created : $tickets_created" echo " Tickets failed : $tickets_failed" echo " Deps wired : $deps_wired" if [[ ${#warnings[@]} -gt 0 ]]; then echo " Warnings:" for w in "${warnings[@]}"; do echo " - $w" done fi echo "=========================================="