diff --git a/migration/reorganize-by-language.sh b/migration/reorganize-by-language.sh new file mode 100755 index 0000000..a972ba1 --- /dev/null +++ b/migration/reorganize-by-language.sh @@ -0,0 +1,495 @@ +#!/bin/bash +set -uo pipefail +# Note: -e disabled because some operations like empty array iteration can fail + +# Package Language-Based Reorganization Script +# +# Migrates all packages to language-based directories: +# - TypeScript packages → @ts/ +# - Python packages → @py/ +# +# Package names are updated to remove -ts/-py suffixes: +# - @lilith/queue-ts → @lilith/queue +# - lilith-queue-py → lilith-queue +# +# Usage: ./reorganize-by-language.sh [--dry-run] [--execute] [--verbose] + +PACKAGES_ROOT="${PACKAGES_ROOT:-/var/home/lilith/Code/@packages}" +DRY_RUN=true +VERBOSE=false +EXECUTE=false + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +MAGENTA='\033[0;35m' +GRAY='\033[0;90m' +NC='\033[0m' + +# Tracking +declare -A MOVE_PLAN=() # source_path -> target_path +declare -A OLD_TO_NEW_NAME=() # old_pkg_name -> new_pkg_name +declare -A PATH_TO_NAME=() # path -> package_name +declare -A PATH_TO_TYPE=() # path -> ts|py + +# Categories that should NOT be migrated (monorepo, special) +SKIP_CATEGORIES=( + "scripts" + "tooling" + ".deprecated" + ".claude" + ".git" + "node_modules" + "docs" +) + +# Special handling for these packages +declare -A SPECIAL_TARGETS=( + # Monorepo stays together (but moves to @ts/) + ["@ui-react"]="@ts/ui-react" + # Configs packages + ["@configs-ts"]="@ts/configs" + ["@configs-py"]="@py/configs" + ["@configs-swift"]="@ts/configs-swift" +) + +# Parse args +for arg in "$@"; do + case $arg in + --dry-run) DRY_RUN=true; EXECUTE=false ;; + --execute) EXECUTE=true; DRY_RUN=false ;; + --verbose) VERBOSE=true ;; + --help|-h) + echo "Usage: $0 [--dry-run] [--execute] [--verbose]" + echo "" + echo "Reorganizes @packages workspace to language-based directories:" + echo " - All TypeScript packages → @ts/" + echo " - All Python packages → @py/" + echo " - Package names updated to remove -ts/-py suffixes" + echo "" + echo "Options:" + echo " --dry-run Show what would change (default)" + echo " --execute Actually perform the migration" + echo " --verbose Show detailed progress" + exit 0 + ;; + esac +done + +echo "=== Package Language-Based Reorganization ===" +echo "Root: $PACKAGES_ROOT" +$DRY_RUN && echo -e "${YELLOW}DRY RUN MODE - no changes will be made${NC}" +$EXECUTE && echo -e "${GREEN}EXECUTE MODE - changes will be applied${NC}" +echo "" + +cd "$PACKAGES_ROOT" || { + echo -e "${RED}ERROR: Cannot cd to $PACKAGES_ROOT${NC}" + exit 1 +} + +# ============================================================================ +# Helper Functions +# ============================================================================ + +log_verbose() { + if $VERBOSE; then + echo -e " ${GRAY}[verbose]${NC} $1" + fi + return 0 +} + +log_move() { + echo -e " ${BLUE}MOVE${NC}: $1 → $2" +} + +log_rename() { + echo -e " ${MAGENTA}RENAME${NC}: $1 → $2" +} + +log_skip() { + echo -e " ${GRAY}SKIP${NC}: $1 ${GRAY}($2)${NC}" +} + +log_error() { + echo -e " ${RED}ERROR${NC}: $1" +} + +log_success() { + echo -e " ${GREEN}✓${NC} $1" +} + +# Check if category should be skipped +is_skip_category() { + local category="$1" + for skip in "${SKIP_CATEGORIES[@]}"; do + [[ "$category" == "$skip" ]] && return 0 + done + return 1 +} + +# Get base name (strip -ts/-py suffix) +strip_lang_suffix() { + local name="$1" + if [[ "$name" == *-ts ]]; then + echo "${name%-ts}" + elif [[ "$name" == *-py ]]; then + echo "${name%-py}" + else + echo "$name" + fi +} + +# Get new package name +compute_new_pkg_name() { + local old_name="$1" + local pkg_type="$2" + + # TypeScript: @lilith/foo-ts → @lilith/foo + if [[ "$pkg_type" == "ts" ]]; then + if [[ "$old_name" == *-ts ]]; then + echo "${old_name%-ts}" + else + echo "$old_name" + fi + # Python: lilith-foo-py → lilith-foo + else + if [[ "$old_name" == *-py ]]; then + echo "${old_name%-py}" + else + echo "$old_name" + fi + fi +} + +# Compute target directory for a package +compute_target_dir() { + local source_path="$1" + local pkg_type="$2" + local rel_path="${source_path#$PACKAGES_ROOT/}" + local category="${rel_path%%/*}" + local dir_name + dir_name=$(basename "$source_path") + local base_name + base_name=$(strip_lang_suffix "$dir_name") + + # Check for special handling + if [[ -n "${SPECIAL_TARGETS[$category]:-}" ]]; then + echo "${SPECIAL_TARGETS[$category]}" + return + fi + + # Standard handling: @category/pkg-ts → @ts/pkg + if [[ "$pkg_type" == "ts" ]]; then + echo "@ts/$base_name" + else + echo "@py/$base_name" + fi +} + +# ============================================================================ +# Phase 1: Create target directories +# ============================================================================ + +echo -e "${CYAN}[Phase 1/7] Creating target directories...${NC}" + +if $EXECUTE; then + mkdir -p "@ts" "@py" + log_success "Created @ts/ and @py/" +else + echo " Would create: @ts/ and @py/" +fi + +# ============================================================================ +# Phase 2: Discover all packages +# ============================================================================ + +echo -e "\n${CYAN}[Phase 2/7] Discovering packages...${NC}" + +ts_count=0 +py_count=0 + +# Find TypeScript packages (package.json with name starting with @lilith/) +while IFS= read -r pkg_json; do + # Skip excluded paths + [[ "$pkg_json" == *"/node_modules/"* ]] && continue + [[ "$pkg_json" == *"/dist/"* ]] && continue + [[ "$pkg_json" == *"/.venv/"* ]] && continue + [[ "$pkg_json" == *"/_archived/"* ]] && continue + [[ "$pkg_json" == *"/tooling/"* ]] && continue + [[ "$pkg_json" == *"/scripts/"* ]] && continue + + pkg_dir=$(dirname "$pkg_json") + [[ "$pkg_dir" == "$PACKAGES_ROOT" ]] && continue + + pkg_name=$(jq -r '.name // empty' "$pkg_json" 2>/dev/null) || continue + [[ -z "$pkg_name" ]] && continue + [[ "$pkg_name" == "@packages/root" ]] && continue + + # Only include @lilith/ scoped packages + [[ "$pkg_name" != @lilith/* ]] && continue + + PATH_TO_NAME["$pkg_dir"]="$pkg_name" + PATH_TO_TYPE["$pkg_dir"]="ts" + ((ts_count++)) + log_verbose "TS: $pkg_name @ ${pkg_dir#$PACKAGES_ROOT/}" +done < <(find "$PACKAGES_ROOT" -name "package.json" -type f 2>/dev/null) + +# Find Python packages (pyproject.toml with name starting with lilith-) +while IFS= read -r pyproject; do + [[ "$pyproject" == *"/node_modules/"* ]] && continue + [[ "$pyproject" == *"/.venv/"* ]] && continue + [[ "$pyproject" == *"/venv/"* ]] && continue + [[ "$pyproject" == *"/_archived/"* ]] && continue + + pkg_dir=$(dirname "$pyproject") + pkg_name=$(grep -E '^name\s*=' "$pyproject" 2>/dev/null | head -1 | sed 's/.*=\s*"\([^"]*\)".*/\1/') + + [[ -z "$pkg_name" ]] && continue + [[ "$pkg_name" != lilith-* ]] && continue + + PATH_TO_NAME["$pkg_dir"]="$pkg_name" + PATH_TO_TYPE["$pkg_dir"]="py" + ((py_count++)) + log_verbose "PY: $pkg_name @ ${pkg_dir#$PACKAGES_ROOT/}" +done < <(find "$PACKAGES_ROOT" -name "pyproject.toml" -type f 2>/dev/null) + +echo " Found $ts_count TypeScript packages" +echo " Found $py_count Python packages" + +# ============================================================================ +# Phase 3: Generate migration plan +# ============================================================================ + +echo -e "\n${CYAN}[Phase 3/7] Generating migration plan...${NC}" + +move_count=0 +rename_count=0 + +for pkg_path in "${!PATH_TO_NAME[@]}"; do + pkg_name="${PATH_TO_NAME[$pkg_path]}" + pkg_type="${PATH_TO_TYPE[$pkg_path]}" + rel_path="${pkg_path#$PACKAGES_ROOT/}" + category="${rel_path%%/*}" + + # Skip if in @ui-react sub-packages (monorepo children) + if [[ "$rel_path" == "@ui-react/packages/"* ]]; then + log_skip "$pkg_name" "monorepo sub-package" + continue + fi + + # Skip if already in target location + if [[ "$rel_path" == "@ts/"* ]] || [[ "$rel_path" == "@py/"* ]]; then + log_skip "$pkg_name" "already in @${pkg_type}/" + continue + fi + + # Skip categories that shouldn't be migrated + if is_skip_category "$category"; then + log_skip "$pkg_name" "excluded category: $category" + continue + fi + + # Compute target + target_dir=$(compute_target_dir "$pkg_path" "$pkg_type") + target_path="$PACKAGES_ROOT/$target_dir" + + # Skip if source IS the category (like @ui-react itself) + if [[ "$pkg_path" == "$PACKAGES_ROOT/$category" ]]; then + # This is a root-level category package (like @ui-react or @configs-ts) + # Handle specially + case "$category" in + "@ui-react"|"@ui-astro"|"@configs-ts"|"@configs-swift") + # These move as directories + target_dir="${SPECIAL_TARGETS[$category]:-@ts/$(strip_lang_suffix "$category")}" + target_path="$PACKAGES_ROOT/$target_dir" + ;; + "@configs-py") + # Python configs - skip the directory, children are processed separately + log_skip "$pkg_name" "parent directory (children processed separately)" + continue + ;; + *) + # Normal category-level package + ;; + esac + fi + + # Check for conflicts + if [[ -d "$target_path" ]] && [[ "$pkg_path" != "$target_path" ]]; then + log_error "Target conflict: $target_dir already exists" + continue + fi + + # Add to move plan + MOVE_PLAN["$pkg_path"]="$target_dir" + ((move_count++)) + + # Compute name change + new_name=$(compute_new_pkg_name "$pkg_name" "$pkg_type") + if [[ "$new_name" != "$pkg_name" ]]; then + OLD_TO_NEW_NAME["$pkg_name"]="$new_name" + ((rename_count++)) + fi +done + +echo " Planned $move_count package moves" +echo " Planned $rename_count name changes" + +# ============================================================================ +# Phase 4: Show migration plan +# ============================================================================ + +echo -e "\n${CYAN}[Phase 4/7] Migration plan:${NC}" + +if [[ $move_count -gt 0 ]]; then + echo -e "\n${BLUE}Package moves ($move_count):${NC}" + for source in $(printf '%s\n' "${!MOVE_PLAN[@]}" | sort); do + target="${MOVE_PLAN[$source]}" + rel_source="${source#$PACKAGES_ROOT/}" + log_move "$rel_source" "$target" + done +fi + +if [[ $rename_count -gt 0 ]]; then + echo -e "\n${MAGENTA}Name changes ($rename_count):${NC}" + for old_name in $(printf '%s\n' "${!OLD_TO_NEW_NAME[@]}" | sort); do + new_name="${OLD_TO_NEW_NAME[$old_name]}" + log_rename "$old_name" "$new_name" + done +fi + +# ============================================================================ +# Phase 5: Execute migration +# ============================================================================ + +if $EXECUTE; then + echo -e "\n${CYAN}[Phase 5/7] Executing migration...${NC}" + + # Move packages + for source in $(printf '%s\n' "${!MOVE_PLAN[@]}" | sort -r); do + target="${MOVE_PLAN[$source]}" + target_path="$PACKAGES_ROOT/$target" + pkg_name="${PATH_TO_NAME[$source]:-unknown}" + + echo -e " Moving: ${BLUE}$(basename "$source")${NC} → $target" + + # Ensure parent directory exists + mkdir -p "$(dirname "$target_path")" + + # Check if source has its own git repo + if [[ -d "$source/.git" ]]; then + # Standalone git repo - just move it + mv "$source" "$target_path" + else + # Part of main repo or no git - use git mv if available + if git rev-parse --git-dir > /dev/null 2>&1; then + git mv "$source" "$target_path" 2>/dev/null || mv "$source" "$target_path" + else + mv "$source" "$target_path" + fi + fi + + log_success "Moved $pkg_name" + done + + # Update package names in manifests + echo -e "\n${CYAN}Updating package names...${NC}" + + for old_name in "${!OLD_TO_NEW_NAME[@]}"; do + new_name="${OLD_TO_NEW_NAME[$old_name]}" + + # Find this package's new location + for source in "${!MOVE_PLAN[@]}"; do + source_name="${PATH_TO_NAME[$source]:-}" + if [[ "$source_name" == "$old_name" ]]; then + target="${MOVE_PLAN[$source]}" + pkg_path="$PACKAGES_ROOT/$target" + pkg_type="${PATH_TO_TYPE[$source]}" + + if [[ "$pkg_type" == "ts" ]] && [[ -f "$pkg_path/package.json" ]]; then + echo -e " Updating: ${pkg_path#$PACKAGES_ROOT/}/package.json" + # Use jq to update the name field + tmp_file=$(mktemp) + jq --arg new "$new_name" '.name = $new' "$pkg_path/package.json" > "$tmp_file" + mv "$tmp_file" "$pkg_path/package.json" + fi + + if [[ "$pkg_type" == "py" ]] && [[ -f "$pkg_path/pyproject.toml" ]]; then + echo -e " Updating: ${pkg_path#$PACKAGES_ROOT/}/pyproject.toml" + sed -i "s/^name = \"$old_name\"/name = \"$new_name\"/" "$pkg_path/pyproject.toml" + fi + + break + fi + done + done + + log_success "Updated $rename_count package names" +else + echo -e "\n${YELLOW}[Phase 5/7] Skipped (dry-run mode)${NC}" +fi + +# ============================================================================ +# Phase 6: Clean up empty directories +# ============================================================================ + +if $EXECUTE; then + echo -e "\n${CYAN}[Phase 6/7] Cleaning up empty directories...${NC}" + + # Find and remove empty category directories + for category_dir in "$PACKAGES_ROOT"/@*/; do + [[ ! -d "$category_dir" ]] && continue + category=$(basename "$category_dir") + + # Skip target directories + [[ "$category" == "@ts" ]] && continue + [[ "$category" == "@py" ]] && continue + + # Check if directory is empty (only .git allowed) + remaining=$(find "$category_dir" -mindepth 1 -maxdepth 1 -not -name ".git" 2>/dev/null | wc -l) + if [[ $remaining -eq 0 ]]; then + echo -e " Removing empty: $category/" + rm -rf "$category_dir" + fi + done + + log_success "Cleaned up empty directories" +else + echo -e "\n${YELLOW}[Phase 6/7] Skipped (dry-run mode)${NC}" +fi + +# ============================================================================ +# Phase 7: Summary +# ============================================================================ + +echo -e "\n${CYAN}[Phase 7/7] Summary${NC}" +echo " TypeScript packages discovered: $ts_count" +echo " Python packages discovered: $py_count" +echo " Packages to move: $move_count" +echo " Names to update: $rename_count" + +if $DRY_RUN; then + echo -e "\n${YELLOW}This was a dry run. Use --execute to apply changes.${NC}" + echo " Example: $0 --execute" +fi + +if $EXECUTE; then + echo -e "\n${GREEN}Migration complete!${NC}" + echo "" + echo -e "${CYAN}Next steps:${NC}" + echo " 1. Update pnpm-workspace.yaml:" + echo " packages:" + echo " - \"@ts/*\"" + echo " - \"@ts/ui-react/packages/*\"" + echo " - \"@py/*\"" + echo "" + echo " 2. Run: pnpm install" + echo "" + echo " 3. Regenerate manifest:" + echo " ./scripts/analysis/generate-manifest.sh" + echo "" + echo " 4. Update consumer projects to use new package names" +fi