Repository: AlaskaAirlines/auro-cli
Pull request #63: Fix auro cli internal scripts
Author: login: rmenner
Labels: name: released
description: Completed work has been released
name: released on @rc
Created: 2025-05-01T02:26:49Z | Merged: 2025-05-02T15:46:36Z

# Alaska Airlines Pull Request

## Type of change:

Please delete options that are not relevant.

- [ ] New capability
- [x] Revision of an existing capability
- [ ] Infrastructure change (automation, etc.)
- [ ] Other (please elaborate)

## Checklist:

- [x] My update follows the CONTRIBUTING guidelines of this project
- [x] I have performed a self-review of my own update

**By submitting this Pull Request, I confirm that my contribution is made under the terms of the Apache 2.0 license.**

_Pull Requests will be evaluated by their quality of update and whether it is consistent with the goals and values of this project. Any submission is to be considered a conversation between the submitter and the maintainers of this project and may require changes to your submission._

**Thank you for your submission!**<br>
-- Auro Design System Team

## Summary by Sourcery

Refactor the Auro CLI build system to improve modularity, build process, and file watching capabilities

Enhancements:
- Modularized build configuration and migration processing
- Improved file watching mechanism for migrations and source files
- Simplified build script with more flexible configuration

Chores:
- Reorganized build scripts into separate modules
- Updated TypeScript configuration
- Simplified package.json scripts


Diff:
diff --git a/.github/workflows/sync-tags.yml b/.github/workflows/sync-tags.yml
new file mode 100644
index 0000000..dd99643
--- /dev/null
+++ b/.github/workflows/sync-tags.yml
@@ -0,0 +1,28 @@
+name: Sync Tags to Dev Branch
+
+on:
+  push:
+    tags:
+      - "*"
+
+jobs:
+  sync-tags:
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout Code
+        uses: actions/checkout@v3
+
+      - name: Push Tag to Dev Branch
+        run: |
+          # Get the current tag
+          TAG=$(git describe --tags --exact-match)
+          echo "Syncing tag $TAG to dev branch"
+
+          # Push the tag to the dev branch
+          git fetch --all
+          git checkout dev
+            git merge --ff-only $TAG
+            git push origin dev
+        env:
+            GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
\ No newline at end of file
diff --git a/build-scripts/build-config.js b/build-scripts/build-config.js
new file mode 100644
index 0000000..db5a0f6
--- /dev/null
+++ b/build-scripts/build-config.js
@@ -0,0 +1,103 @@
+import { resolve } from "node:path";
+import { dirname } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+const projectRoot = resolve(__dirname, "..");
+
+// Core Node.js modules that should remain external
+export const nodeBuiltins = [
+  "node:path",
+  "node:process",
+  "node:fs",
+  "node:child_process",
+  "node:fs/promises",
+  "node:url",
+  "node:util",
+  "path",
+  "fs",
+  "util",
+  "process",
+  "url",
+  "child_process",
+  "fs/promises",
+];
+
+// List of external dependencies that should not be bundled
+export const externalDependencies = [
+  "@aurodesignsystem/auro-library/*",
+  "commander",
+  "@web/dev-server",
+  "@open-wc/dev-server-hmr",
+  "gradient-string",
+  "figlet",
+  "inquirer",
+  "simple-git",
+  "glob",
+  "web-component-analyzer",
+];
+
+// Create the final list of external packages
+export const externalPackages = [...nodeBuiltins, ...externalDependencies];
+
+// Import aliases for bundling
+export const aliases = {
+  "#configs": resolve(projectRoot, "src/configs"),
+  "#commands": resolve(projectRoot, "src/commands"),
+  "#scripts": resolve(projectRoot, "src/scripts"),
+  "#utils": resolve(projectRoot, "src/utils"),
+};
+
+// Function to parse command line arguments
+export function parseArgs() {
+  return {
+    isWatchMode:
+      process.argv.includes("--watch") || process.argv.includes("-w"),
+    isDevelopmentMode: process.argv.includes("--dev"),
+  };
+}
+
+// Function to get build config based on development mode
+export function getBuildConfig(isDev) {
+  const config = {
+    entryPoints: ["src/index.ts"],
+    bundle: true,
+    platform: "node",
+    target: "node18",
+    outfile: "dist/auro-cli.js",
+    format: "esm",
+    banner: {
+      js: "#!/usr/bin/env node",
+    },
+    external: [
+      ...externalPackages,
+      "./migrations/*",
+      "../migrations/*",
+      "node:*",
+    ],
+    packages: "external",
+    loader: {
+      ".node": "file",
+    },
+    mainFields: ["module", "main"],
+    metafile: true,
+    allowOverwrite: true,
+    alias: aliases,
+  };
+
+  if (isDev) {
+    config.minify = false;
+    config.sourcemap = true;
+    config.logLevel = "debug";
+    config.treeShaking = false;
+  } else {
+    config.minify = true;
+    config.sourcemap = "external";
+    config.logLevel = "info";
+    config.treeShaking = true;
+    config.legalComments = "none";
+  }
+
+  return config;
+}
diff --git a/build-scripts/migration-handler.js b/build-scripts/migration-handler.js
new file mode 100644
index 0000000..932f271
--- /dev/null
+++ b/build-scripts/migration-handler.js
@@ -0,0 +1,360 @@
+import crypto from "node:crypto";
+import fs from "node:fs";
+import { watch } from "node:fs";
+import { dirname, join } from "node:path";
+import { build } from "esbuild";
+import ora from "ora";
+
+// Store file hashes for detecting changes
+const fileHashes = new Map();
+
+/**
+ * Sets executable permissions for shell scripts
+ * @param {string} filePath - The path to the file
+ * @param {string} outputFile - The output file path
+ */
+function setExecutablePermissions(filePath, outputFile) {
+  if (filePath.endsWith(".sh")) {
+    try {
+      fs.chmodSync(outputFile, 0o755);
+    } catch (chmodError) {
+      console.error(
+        `⚠️ Warning: Failed to set executable permissions on ${outputFile}:`,
+        chmodError,
+      );
+    }
+  }
+}
+
+/**
+ * Processes a single migration file that has changed
+ * @param {string} filePath - The path to the file that changed
+ * @param {boolean} isDev - Whether to use development build settings
+ * @param {string[]} externalPackages - List of packages to keep external
+ * @param {Object} aliases - Map of import aliases
+ * @returns {Promise<boolean>} - Whether the processing was successful
+ */
+export async function processMigrationFile(
+  filePath,
+  { isDev = false, externalPackages = [], aliases = {} },
+) {
+  try {
+    if (!fs.existsSync(filePath)) {
+      console.log(`❌ File no longer exists: ${filePath}`);
+      return false;
+    }
+
+    const fileSpinner = ora(`Processing ${filePath}...`).start();
+
+    // Calculate output path
+    const outputFile = filePath.replace("src/migrations", "dist/migrations");
+    const outputDir = dirname(outputFile);
+
+    // Ensure output directory exists
+    if (!fs.existsSync(outputDir)) {
+      fs.mkdirSync(outputDir, { recursive: true });
+    }
+
+    if (filePath.endsWith(".js")) {
+      // Process JS file - bundle it
+      const migrationBuildConfig = {
+        entryPoints: [filePath],
+        bundle: true,
+        platform: "node",
+        target: "node18",
+        outfile: outputFile,
+        format: "esm",
+        external: [...externalPackages, "node:*"],
+        packages: "external",
+        allowOverwrite: true,
+        alias: aliases,
+      };
+
+      if (isDev) {
+        migrationBuildConfig.minify = false;
+        migrationBuildConfig.sourcemap = true;
+        migrationBuildConfig.logLevel = "debug";
+        migrationBuildConfig.treeShaking = false;
+      } else {
+        migrationBuildConfig.minify = true;
+        migrationBuildConfig.sourcemap = false;
+        migrationBuildConfig.legalComments = "none";
+        migrationBuildConfig.treeShaking = true;
+      }
+
+      await build(migrationBuildConfig);
+    } else if (filePath.endsWith(".yml") || filePath.endsWith(".sh")) {
+      // Copy configuration file
+      fs.copyFileSync(filePath, outputFile);
+
+      // Set executable permissions for shell scripts
+      setExecutablePermissions(filePath, outputFile);
+    }
+
+    fileSpinner.succeed(`Successfully processed ${filePath}`);
+
+    // Update the file hash
+    const fileHash = crypto
+      .createHash("sha256")
+      .update(fs.readFileSync(filePath))
+      .digest("hex");
+    fileHashes.set(filePath, fileHash);
+
+    return true;
+  } catch (error) {
+    console.error(`❌ Error processing ${filePath}:`, error);
+    return false;
+  }
+}
+
+/**
+ * Sets up a watcher for migration files
+ * @param {Object} options - Configuration options
+ * @param {boolean} options.isDev - Whether to use development build settings
+ * @param {string[]} options.externalPackages - List of packages to keep external
+ * @param {Object} options.aliases - Map of import aliases
+ * @param {Object} options.onEvent - Optional callback for file change events
+ * @returns {Object} The file system watchers
+ */
+export function watchMigrationFiles(options = {}) {
+  // Initialize hash tracking for existing files
+  function initializeFileHashes(dir) {
+    if (!fs.existsSync(dir)) return;
+
+    const files = fs.readdirSync(dir);
+    for (const file of files) {
+      const filePath = join(dir, file);
+      if (fs.statSync(filePath).isDirectory()) {
+        initializeFileHashes(filePath);
+      } else if (
+        filePath.endsWith(".js") ||
+        filePath.endsWith(".yml") ||
+        filePath.endsWith(".sh") ||
+        filePath.endsWith(".ts")
+      ) {
+        try {
+          const fileHash = crypto
+            .createHash("sha256")
+            .update(fs.readFileSync(filePath))
+            .digest("hex");
+          fileHashes.set(filePath, fileHash);
+        } catch (error) {
+          console.error(`❌ Error hashing ${filePath}:`, error);
+        }
+      }
+    }
+  }
+
+  // Initialize hashes for existing files
+  initializeFileHashes("src/migrations");
+  initializeFileHashes("src");
+
+  const watchers = {
+    // Set up the watcher for migrations
+    migrations: watch(
+      "src/migrations",
+      { recursive: true },
+      async (eventType, filename) => {
+        if (!filename) return;
+
+        const filePath = join("src/migrations", filename);
+
+        // Skip if file doesn't exist (might have been deleted)
+        if (!fs.existsSync(filePath)) {
+          console.log(`❌ File no longer exists: ${filePath}`);
+          return;
+        }
+
+        // Skip if not a file type we care about
+        if (
+          !filePath.endsWith(".js") &&
+          !filePath.endsWith(".yml") &&
+          !filePath.endsWith(".sh")
+        ) {
+          return;
+        }
+
+        // Check if content actually changed by comparing hashes
+        try {
+          const currentHash = crypto
+            .createHash("sha256")
+            .update(fs.readFileSync(filePath))
+            .digest("hex");
+
+          const previousHash = fileHashes.get(filePath);
+
+          // Only process if file is new or contents changed
+          if (previousHash !== currentHash) {
+            console.log(`🔄 Detected change in ${filename}`);
+            await processMigrationFile(filePath, options);
+          }
+        } catch (error) {
+          console.error(`❌ Error checking file ${filePath}:`, error);
+        }
+      },
+    ),
+
+    // Set up the watcher for the entire src directory
+    src: watch("src", { recursive: true }, async (eventType, filename) => {
+      if (!filename) return;
+
+      // Skip migrations directory as it's handled by the other watcher
+      if (filename.startsWith("migrations/")) return;
+
+      const filePath = join("src", filename);
+
+      // Skip if file doesn't exist (might have been deleted)
+      if (!fs.existsSync(filePath)) {
+        console.log(`❌ File no longer exists: ${filePath}`);
+        return;
+      }
+
+      // Skip if not a file type we care about
+      if (
+        !filePath.endsWith(".js") &&
+        !filePath.endsWith(".ts") &&
+        !filePath.endsWith(".mjs") &&
+        !filePath.endsWith(".cjs")
+      ) {
+        return;
+      }
+
+      try {
+        const currentHash = crypto
+          .createHash("sha256")
+          .update(fs.readFileSync(filePath))
+          .digest("hex");
+
+        const previousHash = fileHashes.get(filePath);
+
+        // Only process if file is new or contents changed
+        if (previousHash !== currentHash) {
+          console.log(`🔄 Detected change in ${filename}`);
+          fileHashes.set(filePath, currentHash);
+
+          // Trigger a rebuild of the main bundle
+          if (typeof options.onEvent === "function") {
+            options.onEvent({
+              type: "change",
+              path: filePath,
+              requiresRebuild: true,
+            });
+          }
+        }
+      } catch (error) {
+        console.error(`❌ Error checking file ${filePath}:`, error);
+        if (typeof options.onEvent === "function") {
+          options.onEvent({ error });
+        }
+      }
+    }),
+  };
+
+  // Return an object with both watchers
+  return {
+    close: () => {
+      watchers.migrations.close();
+      watchers.src.close();
+    },
+  };
+}
+
+/**
+ * Process all migration files
+ * @param {Object} options - Configuration options
+ * @param {boolean} options.isDev - Whether to use development build settings
+ * @param {string[]} options.externalPackages - List of packages to keep external
+ * @param {Object} options.aliases - Map of import aliases
+ */
+export async function processMigrations(options = {}) {
+  try {
+    // Step 1: Process migration JS files to inline their dependencies
+    const migrationJsFiles = [];
+
+    /**
+     * Recursively finds all JavaScript files in a given directory.
+     * @param {string} dir - The directory to search for JavaScript files.
+     */
+    function findJsFiles(dir) {
+      if (!fs.existsSync(dir)) {
+        return;
+      }
+      const files = fs.readdirSync(dir);
+      for (const file of files) {
+        const filePath = join(dir, file);
+        if (fs.statSync(filePath).isDirectory()) {
+          findJsFiles(filePath);
+        } else if (file.endsWith(".js")) {
+          migrationJsFiles.push(filePath);
+        }
+      }
+    }
+
+    findJsFiles("src/migrations");
+
+    // Process each migration JS file
+    const migrationSpinner = ora(
+      `Processing ${migrationJsFiles.length} migration JS files...`,
+    ).start();
+
+    for (const migrationFile of migrationJsFiles) {
+      await processMigrationFile(migrationFile, options);
+    }
+
+    migrationSpinner.succeed(
+      `Processed ${migrationJsFiles.length} migration JS files.`,
+    );
+
+    // Step 2: Copy migration .yml and .sh files
+    const copySpinner = ora(
+      "Copying migration configuration files (.yml, .sh)...",
+    ).start();
+
+    /**
+     * Recursively finds and copies all .yml and .sh files from a source directory to a destination directory.
+     * @param {string} srcDir - The source directory to search in.
+     * @param {string} destDir - The destination directory to copy to.
+     */
+    function copyMigrationConfigFiles(srcDir, destDir) {
+      if (!fs.existsSync(srcDir)) {
+        return 0;
+      }
+
+      let copiedCount = 0;
+      const items = fs.readdirSync(srcDir);
+
+      for (const item of items) {
+        const srcPath = join(srcDir, item);
+        const destPath = join(destDir, item);
+
+        if (fs.statSync(srcPath).isDirectory()) {
+          if (!fs.existsSync(destPath)) {
+            fs.mkdirSync(destPath, { recursive: true });
+          }
+          copiedCount += copyMigrationConfigFiles(srcPath, destPath);
+        } else if (item.endsWith(".yml") || item.endsWith(".sh")) {
+          // Always copy the file regardless if it exists or has changed
+          fs.copyFileSync(srcPath, destPath);
+
+          // Set executable permissions for shell scripts
+          setExecutablePermissions(srcPath, destPath);
+
+          copiedCount++;
+        }
+      }
+
+      return copiedCount;
+    }
+
+    const copiedFiles = copyMigrationConfigFiles(
+      "src/migrations",
+      "dist/migrations",
+    );
+    copySpinner.succeed(
+      `Copied ${copiedFiles} migration configuration files (.yml, .sh).`,
+    );
+  } catch (error) {
+    console.error("❌ Failed to process migrations:", error);
+    throw error;
+  }
+}
diff --git a/esbuild.config.js b/esbuild.config.js
index 762e98f..55a79c5 100644
--- a/esbuild.config.js
+++ b/esbuild.config.js
@@ -1,52 +1,23 @@
 import fs from "node:fs";
-import { dirname, join, resolve } from "node:path";
+import { join } from "node:path";
 import process from "node:process";
-import { fileURLToPath } from "node:url";
-/* eslint-disable no-underscore-dangle, no-await-in-loop, no-magic-numbers */
 import { build } from "esbuild";
 import ora from "ora";
 
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = dirname(__filename);
-
-// Core Node.js modules that should remain external
-const nodeBuiltins = [
-  "node:path",
-  "node:process",
-  "node:fs",
-  "node:child_process",
-  "node:fs/promises",
-  "node:url",
-  "node:util",
-  "path",
-  "fs",
-  "util",
-  "process",
-  "url",
-  "child_process",
-  "fs/promises",
-];
-
-// List of external dependencies that should not be bundled
-const externalDependencies = [
-  // Core external dependencies that should remain separate
-  "@aurodesignsystem/auro-library/*",
-  "commander",
-  "@web/dev-server",
-  "@open-wc/dev-server-hmr",
-  "gradient-string",
-  "figlet",
-  "inquirer",
-  "simple-git",
-  "glob",
-  "web-component-analyzer",
-];
-
-// Create the final list of external packages
-const externalPackages = [...nodeBuiltins, ...externalDependencies];
-
-// Parse command line arguments to check for development mode
-const isDevelopmentMode = process.argv.includes("--dev");
+// Import our modularized build scripts
+import {
+  aliases,
+  externalPackages,
+  getBuildConfig,
+  parseArgs,
+} from "./build-scripts/build-config.js";
+import {
+  processMigrations,
+  watchMigrationFiles,
+} from "./build-scripts/migration-handler.js";
+
+// Parse command line arguments
+const { isWatchMode, isDevelopmentMode } = parseArgs();
 
 // Custom build steps for optimizing the distribution
 /**
@@ -60,6 +31,11 @@ async function runBuild(isDev = false) {
       fs.mkdirSync("dist", { recursive: true });
     }
 
+    // Ensure migrations directory exists
+    if (!fs.existsSync("dist/migrations")) {
+      fs.mkdirSync("dist/migrations", { recursive: true });
+    }
+
     // Step 2: Clean non-migration files from dist (if they exist)
     const preserveMigrations = (item) => item !== "migrations";
     const items = fs.readdirSync("dist").filter(preserveMigrations);
@@ -74,64 +50,15 @@ async function runBuild(isDev = false) {
       }
     }
 
-    // Step 3: Bundle the application in a single file
+    // Step 3: Build the main CLI bundle
     const buildSpinner = ora(
       isDev
         ? "Building in DEVELOPMENT mode..."
         : "Building in PRODUCTION mode...",
     ).start();
 
-    const buildConfig = {
-      entryPoints: ["src/index.js"],
-      bundle: true,
-      platform: "node",
-      target: "node18",
-      outfile: "dist/auro-cli.js",
-      format: "esm",
-      banner: {
-        js: "#!/usr/bin/env node",
-      },
-      external: [
-        ...externalPackages,
-        // Ensure migrations are kept external
-        "./migrations/*",
-        "../migrations/*",
-        // Handle dynamic requires of node modules
-        "node:*",
-      ],
-      // Prevent bundling Node.js built-ins
-      packages: "external",
-      loader: {
-        ".node": "file",
-      },
-      mainFields: ["module", "main"],
-      metafile: true,
-      allowOverwrite: true,
-      // Support aliased imports from package.json
-      alias: {
-        "#configs": resolve(__dirname, "src/configs"),
-        "#commands": resolve(__dirname, "src/commands"),
-        "#scripts": resolve(__dirname, "src/scripts"),
-        "#utils": resolve(__dirname, "src/utils"),
-      },
-    };
-
-    // Apply development or production specific settings
-    if (isDev) {
-      // Development mode settings for easier debugging
-      buildConfig.minify = false;
-      buildConfig.sourcemap = true;
-      buildConfig.logLevel = "debug";
-      buildConfig.treeShaking = false;
-      // No aggressive bundling in dev mode
-    } else {
-      // Production mode settings
-      buildConfig.minify = true;
-      buildConfig.sourcemap = "external";
-      buildConfig.logLevel = "info";
-      buildConfig.treeShaking = true;
-      buildConfig.legalComments = "none";
-    }
+    // Get the build configuration
+    const buildConfig = getBuildConfig(isDev);
 
     const result = await build(buildConfig);
 
@@ -141,101 +68,15 @@ async function runBuild(isDev = false) {
         : "Build complete with optimized distribution!",
     );
 
-    // Step 4: Process migration JS files to inline their dependencies
-    // Find all JS files in the migrations folder
-    const migrationJsFiles = [];
-
-    /**
-     * Recursively finds all JavaScript files in a given directory.
-     * @param {string} dir - The directory to search for JavaScript files.
-     */
-    function findJsFiles(dir) {
-      if (!fs.existsSync(dir)) {
-        return;
-      }
-      const files = fs.readdirSync(dir);
-      for (const file of files) {
-        const filePath = join(dir, file);
-        if (fs.statSync(filePath).isDirectory()) {
-          findJsFiles(filePath);
-        } else if (file.endsWith(".js")) {
-          migrationJsFiles.push(filePath);
-        }
-      }
-    }
-
-    // Find all JS files in src/migrations
-    findJsFiles("src/migrations");
-
-    // Process each migration JS file
-    const migrationSpinner = ora(
-      `Processing ${migrationJsFiles.length} migration JS files...`,
-    ).start();
-
-    for (const migrationFile of migrationJsFiles) {
-      // Calculate output path - replacing 'src/migrations' with 'dist/migrations'
-      const outputFile = migrationFile.replace(
-        "src/migrations",
-        "dist/migrations",
-      );
-
-      // Ensure directory exists
-      const outputDir = dirname(outputFile);
-      if (!fs.existsSync(outputDir)) {
-        fs.mkdirSync(outputDir, { recursive: true });
-      }
-
-      // Bundle the migration file with its dependencies
-      const migrationBuildConfig = {
-        entryPoints: [migrationFile],
-        bundle: true,
-        platform: "node",
-        target: "node18",
-        outfile: outputFile,
-        format: "esm",
-        // Keep only external package dependencies
-        external: [
-          ...externalPackages,
-          // Handle dynamic requires of node modules
-          "node:*",
-        ],
-        // Prevent bundling Node.js built-ins
-        packages: "external",
-        allowOverwrite: true,
-        // Support utils/ imports and other local imports
-        alias: {
-          "#utils": resolve(__dirname, "src/utils"),
-          "#scripts": resolve(__dirname, "src/scripts"),
-          "#commands": resolve(__dirname, "src/commands"),
-        },
-      };
-
-      // Apply development or production specific settings for migrations
-      if (isDev) {
-        // Development mode settings for migrations
-        migrationBuildConfig.minify = false;
-        migrationBuildConfig.sourcemap = true;
-        migrationBuildConfig.logLevel = "debug";
-        migrationBuildConfig.treeShaking = false;
-      } else {
-        // Production mode settings for migrations
-        migrationBuildConfig.minify = true;
-        migrationBuildConfig.sourcemap = false;
-        migrationBuildConfig.legalComments = "none";
-        migrationBuildConfig.treeShaking = true;
-      }
-
-      await build(migrationBuildConfig);
+    // Step 4: Report build stats
+    if (result.metafile) {
+      const bundleSize = fs.statSync("dist/auro-cli.js").size;
+      console.log(`🔹 Main bundle size: ${(bundleSize / 1024).toFixed(2)}kb`);
     }
 
-    migrationSpinner.succeed(
-      `Processed ${migrationJsFiles.length} migration JS files.`,
-    );
-
-    // Step 5: Fix any issues with the main bundle
+    // Step 5: Fix permissions on the main bundle
     const mainBundlePath = "dist/auro-cli.js";
     if (fs.existsSync(mainBundlePath)) {
-      // Set executable permissions (rwxr-xr-x)
       try {
         fs.chmodSync(mainBundlePath, 0o755);
         console.log("🔐 Set executable permissions on output file");
@@ -250,10 +91,69 @@ async function runBuild(isDev = false) {
       }
     }
 
-    // Step 6: Report build stats
-    if (result.metafile) {
-      const bundleSize = fs.statSync("dist/auro-cli.js").size;
-      console.log(`🔹 Main bundle size: ${(bundleSize / 1024).toFixed(2)}kb`);
+    // Step 6: Process migrations using our modular handler (always run)
+    const migrationSpinner = ora("Processing migrations...").start();
+    await processMigrations({
+      isDev,
+      externalPackages,
+      aliases,
+    });
+    migrationSpinner.succeed("Migrations processed successfully");
+
+    // Step 7: Watch for file changes if in watch mode
+    if (isWatchMode) {
+      // Create a watch spinner with custom styling
+      const watchSpinner = ora({
+        text: "Waiting for file changes...\n",
+        spinner: "bouncingBar",
+        color: "green",
+      }).start();
+
+      // Use our modular migration file watcher
+      const watcher = watchMigrationFiles({
+        isDev,
+        externalPackages,
+        aliases,
+        onEvent: async (event) => {
+          if (event.error) {
+            watchSpinner.fail(`Watch mode error: ${event.error.message}`);
+            watchSpinner.text = "Waiting for file changes...\n";
+            watchSpinner.start();
+          } else if (event.requiresRebuild) {
+            // If a source file changed that requires a full rebuild
+            watchSpinner.text = `Rebuilding after changes to ${event.path}...`;
+
+            try {
+              // Rebuild the main bundle with the current configuration
+              await build(buildConfig);
+              watchSpinner.succeed(`Rebuild complete for ${event.path}`);
+
+              // Report build stats if available
+              if (fs.existsSync("dist/auro-cli.js")) {
+                const bundleSize = fs.statSync("dist/auro-cli.js").size;
+                console.log(
+                  `🔹 Main bundle size: ${(bundleSize / 1024).toFixed(2)}kb`,
+                );
+              }
+            } catch (buildError) {
+              watchSpinner.fail(`Rebuild failed: ${buildError.message}`);
+            }
+
+            // Reset spinner state
+            watchSpinner.text = "Waiting for file changes...\n";
+            watchSpinner.start();
+          }
+        },
+      });
+
+      // Set up cleanup on process exit
+      process.on("SIGINT", () => {
+        if (watcher) {
+          watcher.close();
+        }
+        watchSpinner.stop();
+        process.exit(0);
+      });
     }
   } catch (error) {
     console.error("❌ Build failed:", error);
@@ -261,5 +161,5 @@ async function runBuild(isDev = false) {
   }
 }
 
-// Execute the build with development mode if --dev flag is present
+// Run the build with the current mode
 runBuild(isDevelopmentMode);
diff --git a/package.json b/package.json
index 697ff26..8d44512 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
     "lint": "biome check --fix --no-errors-on-unmatched",
     "prepare": "husky",
     "test": "echo \"Notice: no tests available\" && exit 0",
-    "dev": "nodemon --watch src --exec 'npm run build:dev'",
+    "dev": "tsc && npm run copy-files && npm run bundle -- --watch --dev",
     "build": "tsc && npm run copy-files && npm run bundle",
     "build:dev": "tsc && npm run copy-files && npm run bundle -- --dev",
     "copy-files": "copyfiles -f -u 1 \"src/migrations/**/*.{yml,sh}\" dist/ && tsc-alias",
diff --git a/src/utils/shell.js b/src/utils/shell.js
index 366ad95..34f7fe6 100644
--- a/src/utils/shell.js
+++ b/src/utils/shell.js
@@ -2,28 +2,38 @@ import { spawn } from "node:child_process";
 import ora from "ora";
 
 const shell = (command, _args) => {
-  const spinner = ora(
-    `Running ${command} ${_args ? _args.join(" ") : ""}`,
-  ).start();
+  const commandString = `${command} ${_args ? _args.join(" ") : ""}`;
+
+  // Initialize the spinner but don't start it - we'll just use it for completion status
+  const spinner = ora();
+
+  // Store command output to display after completion
+  const commandOutput = [];
 
   const child = spawn(command, _args || [], { stdio: "pipe", shell: true });
 
   child.stdout.on("data", (data) => {
-    spinner.text = data.toString();
+    // Convert buffer to string
+    const output = data.toString();
+
+    // Store full output
+    commandOutput.push(output);
+
+    // Output directly to console
+    process.stdout.write(output);
   });
 
   child.stderr.on("data", (data) => {
-    spinner.text = data.toString();
+    const output = data.toString();
+    commandOutput.push(output);
+    process.stderr.write(output);
   });
 
   child.on("close", (code) => {
-    spinner.stop();
     if (code !== 0) {
-      spinner.fail(`${command} ${_args ? _args.join(" ") : ""} (code ${code})`);
+      spinner.fail(`${commandString} failed (code ${code})`);
     } else {
-      spinner.succeed(
-        `${command} ${_args ? _args.join(" ") : ""} (code ${code})`,
-      );
+      spinner.succeed(`${commandString} completed successfully`);
     }
   });
 };
diff --git a/tsconfig.json b/tsconfig.json
index b9e6df5..3cfd2a3 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -120,10 +120,6 @@
     "src/**/*.d.ts",
     "src/**/*.js",
     "src/**/*.yml",
-    "src/**/*.sh",
-    "migration/**/*.ts",
-    "migration/**/*.js",
-    "migration/**/*.yml",
-    "migration/**/*.sh"
+    "src/**/*.sh"
   ]
 }


Reviews:
user: login: sourcery-ai[bot]
body: Hey @rmenner - I've reviewed your changes - here's some feedback:

- The shell utility's output handling has changed to print directly instead of updating the spinner; confirm this new behavior aligns with expectations for command feedback.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 2 issues found
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-01T02:28:22Z
user: login: StephenRiosDev
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T15:41:46Z

Review comments:
diff_hunk: @@ -0,0 +1,360 @@
+import crypto from "node:crypto";
+import fs from "node:fs";
+import { watch } from "node:fs";
+import { dirname, join, resolve } from "node:path";
+import { build } from "esbuild";
+import ora from "ora";
+
+// Store file hashes for detecting changes
+const fileHashes = new Map();
+
+/**
+ * Processes a single migration file that has changed
+ * @param {string} filePath - The path to the file that changed
+ * @param {boolean} isDev - Whether to use development build settings
+ * @param {string[]} externalPackages - List of packages to keep external
+ * @param {Object} aliases - Map of import aliases
+ * @returns {Promise<boolean>} - Whether the processing was successful
+ */
+export async function processMigrationFile(
+  filePath,
+  { isDev = false, externalPackages = [], aliases = {} },
+) {
+  try {
+    if (!fs.existsSync(filePath)) {
+      console.log(`❌ File no longer exists: ${filePath}`);
+      return false;
+    }
+
+    const fileSpinner = ora(`Processing ${filePath}...`).start();
+
+    // Calculate output path
+    const outputFile = filePath.replace("src/migrations", "dist/migrations");
+    const outputDir = dirname(outputFile);
+
+    // Ensure output directory exists
+    if (!fs.existsSync(outputDir)) {
+      fs.mkdirSync(outputDir, { recursive: true });
+    }
+
+    if (filePath.endsWith(".js")) {
+      // Process JS file - bundle it
+      const migrationBuildConfig = {
+        entryPoints: [filePath],
+        bundle: true,
+        platform: "node",
+        target: "node18",
+        outfile: outputFile,
+        format: "esm",
+        external: [...externalPackages, "node:*"],
+        packages: "external",
+        allowOverwrite: true,
+        alias: aliases,
+      };
+
+      if (isDev) {
+        migrationBuildConfig.minify = false;
+        migrationBuildConfig.sourcemap = true;
+        migrationBuildConfig.logLevel = "debug";
+        migrationBuildConfig.treeShaking = false;
+      } else {
+        migrationBuildConfig.minify = true;
+        migrationBuildConfig.sourcemap = false;
+        migrationBuildConfig.legalComments = "none";
+        migrationBuildConfig.treeShaking = true;
+      }
+
+      await build(migrationBuildConfig);
+    } else if (filePath.endsWith(".yml") || filePath.endsWith(".sh")) {
+      // Copy configuration file
+      fs.copyFileSync(filePath, outputFile);
+
+      // Set executable permissions on shell scripts
+      if (filePath.endsWith(".sh")) {
path: build-scripts/migration-handler.js
user: login: sourcery-ai[bot]
body: **suggestion:** Repetitive logic for setting executable permissions.

Extract the executable-permission logic into a shared helper since both processMigrationFile and copyMigrationConfigFiles set shell script permissions.

Suggested implementation:

```javascript
// Helper function to set executable permissions on shell scripts
function setExecutablePermissions(filePath, outputFile) {
  if (filePath.endsWith(".sh")) {
    try {
      fs.chmodSync(outputFile, 0o755);
    } catch (chmodError) {
      console.error(`⚠️ Warning: Failed to set executable permissions on ${outputFile}:`, chmodError);
    }
  }
}

```

```javascript
      fs.copyFileSync(filePath, outputFile);
      setExecutablePermissions(filePath, outputFile);

```

Ensure any other occurrence of setting executable permissions (e.g., in processMigrationFile) is similarly updated to call setExecutablePermissions(filePath, outputFile) instead of duplicating the chmod logic.
created_at: 2025-05-01T02:28:21Z
author_association: NONE
diff_hunk: @@ -0,0 +1,360 @@
+import crypto from "node:crypto";
+import fs from "node:fs";
+import { watch } from "node:fs";
+import { dirname, join, resolve } from "node:path";
+import { build } from "esbuild";
+import ora from "ora";
+
+// Store file hashes for detecting changes
+const fileHashes = new Map();
+
+/**
+ * Processes a single migration file that has changed
+ * @param {string} filePath - The path to the file that changed
+ * @param {boolean} isDev - Whether to use development build settings
+ * @param {string[]} externalPackages - List of packages to keep external
+ * @param {Object} aliases - Map of import aliases
+ * @returns {Promise<boolean>} - Whether the processing was successful
+ */
+export async function processMigrationFile(
+  filePath,
+  { isDev = false, externalPackages = [], aliases = {} },
+) {
+  try {
+    if (!fs.existsSync(filePath)) {
+      console.log(`❌ File no longer exists: ${filePath}`);
+      return false;
+    }
+
+    const fileSpinner = ora(`Processing ${filePath}...`).start();
+
+    // Calculate output path
+    const outputFile = filePath.replace("src/migrations", "dist/migrations");
+    const outputDir = dirname(outputFile);
+
+    // Ensure output directory exists
+    if (!fs.existsSync(outputDir)) {
+      fs.mkdirSync(outputDir, { recursive: true });
+    }
+
+    if (filePath.endsWith(".js")) {
+      // Process JS file - bundle it
+      const migrationBuildConfig = {
+        entryPoints: [filePath],
+        bundle: true,
+        platform: "node",
+        target: "node18",
+        outfile: outputFile,
+        format: "esm",
+        external: [...externalPackages, "node:*"],
+        packages: "external",
+        allowOverwrite: true,
+        alias: aliases,
+      };
+
+      if (isDev) {
+        migrationBuildConfig.minify = false;
+        migrationBuildConfig.sourcemap = true;
+        migrationBuildConfig.logLevel = "debug";
+        migrationBuildConfig.treeShaking = false;
+      } else {
+        migrationBuildConfig.minify = true;
+        migrationBuildConfig.sourcemap = false;
+        migrationBuildConfig.legalComments = "none";
+        migrationBuildConfig.treeShaking = true;
+      }
+
+      await build(migrationBuildConfig);
+    } else if (filePath.endsWith(".yml") || filePath.endsWith(".sh")) {
+      // Copy configuration file
+      fs.copyFileSync(filePath, outputFile);
+
+      // Set executable permissions on shell scripts
+      if (filePath.endsWith(".sh")) {
+        try {
+          fs.chmodSync(outputFile, 0o755);
+        } catch (chmodError) {
+          console.error(
+            `⚠️ Warning: Failed to set executable permissions on ${outputFile}:`,
+            chmodError,
+          );
+        }
+      }
+    }
+
+    fileSpinner.succeed(`Successfully processed ${filePath}`);
+
+    // Update the file hash
+    const fileHash = crypto
+      .createHash("sha256")
+      .update(fs.readFileSync(filePath))
+      .digest("hex");
+    fileHashes.set(filePath, fileHash);
+
+    return true;
+  } catch (error) {
+    console.error(`❌ Error processing ${filePath}:`, error);
+    return false;
+  }
+}
+
+/**
+ * Sets up a watcher for migration files
+ * @param {Object} options - Configuration options
+ * @param {boolean} options.isDev - Whether to use development build settings
+ * @param {string[]} options.externalPackages - List of packages to keep external
+ * @param {Object} options.aliases - Map of import aliases
+ * @param {Object} options.onEvent - Optional callback for file change events
+ * @returns {Object} The file system watchers
+ */
+export function watchMigrationFiles(options = {}) {
+  // Initialize hash tracking for existing files
+  function initializeFileHashes(dir) {
+    if (!fs.existsSync(dir)) return;
+
+    const files = fs.readdirSync(dir);
+    for (const file of files) {
+      const filePath = join(dir, file);
+      if (fs.statSync(filePath).isDirectory()) {
+        initializeFileHashes(filePath);
+      } else if (
+        filePath.endsWith(".js") ||
+        filePath.endsWith(".yml") ||
+        filePath.endsWith(".sh") ||
+        filePath.endsWith(".ts")
+      ) {
+        try {
+          const fileHash = crypto
+            .createHash("sha256")
+            .update(fs.readFileSync(filePath))
+            .digest("hex");
+          fileHashes.set(filePath, fileHash);
+        } catch (error) {
+          console.error(`❌ Error hashing ${filePath}:`, error);
+        }
+      }
+    }
+  }
+
+  // Initialize hashes for existing files
+  initializeFileHashes("src/migrations");
+  initializeFileHashes("src");
+
+  const watchers = {
path: build-scripts/migration-handler.js
user: login: sourcery-ai[bot]
body: **suggestion (performance):** Consider debouncing rapid file change events.

Right now each file change triggers processing immediately; debouncing quick successive events would avoid redundant work or rebuilds.

Suggested implementation:

```javascript
const debounce = require('lodash.debounce');
// (other require/import statements)

```

```javascript
// Initialize hashes for existing files
initializeFileHashes("src/migrations");
initializeFileHashes("src");

const handleMigrationsEvent = debounce(async (eventType, filename) => {
  if (!filename) return;

  const filePath = join("src/migrations", filename);

  // Skip if file doesn't exist (might have been deleted)
  if (!fs.existsSync(filePath)) {
    console.log(`❌ File no longer exists: ${filePath}`);
    return;
  }

  // (The rest of the existing migration event logic goes here)
  ...
}, 300);

const watchers = {
  // Set up the watcher for migrations
  migrations: watch(
    "src/migrations",
    { recursive: true },
    handleMigrationsEvent

```

If there are similar file watchers (for example in the "src" directory), consider applying the same debouncing approach. Also, ensure that the remaining migration event logic inside the debounced function gets transferred from the inline callback to the new handler.
created_at: 2025-05-01T02:28:21Z
author_association: NONE
diff_hunk: @@ -0,0 +1,360 @@
+import crypto from "node:crypto";
+import fs from "node:fs";
+import { watch } from "node:fs";
+import { dirname, join, resolve } from "node:path";
+import { build } from "esbuild";
+import ora from "ora";
+
+// Store file hashes for detecting changes
+const fileHashes = new Map();
+
+/**
+ * Processes a single migration file that has changed
+ * @param {string} filePath - The path to the file that changed
+ * @param {boolean} isDev - Whether to use development build settings
+ * @param {string[]} externalPackages - List of packages to keep external
+ * @param {Object} aliases - Map of import aliases
+ * @returns {Promise<boolean>} - Whether the processing was successful
+ */
+export async function processMigrationFile(
+  filePath,
+  { isDev = false, externalPackages = [], aliases = {} },
+) {
+  try {
+    if (!fs.existsSync(filePath)) {
+      console.log(`❌ File no longer exists: ${filePath}`);
+      return false;
+    }
+
+    const fileSpinner = ora(`Processing ${filePath}...`).start();
+
+    // Calculate output path
+    const outputFile = filePath.replace("src/migrations", "dist/migrations");
+    const outputDir = dirname(outputFile);
+
+    // Ensure output directory exists
+    if (!fs.existsSync(outputDir)) {
+      fs.mkdirSync(outputDir, { recursive: true });
+    }
+
+    if (filePath.endsWith(".js")) {
+      // Process JS file - bundle it
+      const migrationBuildConfig = {
+        entryPoints: [filePath],
+        bundle: true,
+        platform: "node",
+        target: "node18",
+        outfile: outputFile,
+        format: "esm",
+        external: [...externalPackages, "node:*"],
+        packages: "external",
+        allowOverwrite: true,
+        alias: aliases,
+      };
+
+      if (isDev) {
+        migrationBuildConfig.minify = false;
+        migrationBuildConfig.sourcemap = true;
+        migrationBuildConfig.logLevel = "debug";
+        migrationBuildConfig.treeShaking = false;
+      } else {
+        migrationBuildConfig.minify = true;
+        migrationBuildConfig.sourcemap = false;
+        migrationBuildConfig.legalComments = "none";
+        migrationBuildConfig.treeShaking = true;
+      }
+
+      await build(migrationBuildConfig);
+    } else if (filePath.endsWith(".yml") || filePath.endsWith(".sh")) {
+      // Copy configuration file
+      fs.copyFileSync(filePath, outputFile);
+
+      // Set executable permissions on shell scripts
+      if (filePath.endsWith(".sh")) {
+        try {
+          fs.chmodSync(outputFile, 0o755);
+        } catch (chmodError) {
+          console.error(
+            `⚠️ Warning: Failed to set executable permissions on ${outputFile}:`,
+            chmodError,
+          );
+        }
+      }
+    }
+
+    fileSpinner.succeed(`Successfully processed ${filePath}`);
+
+    // Update the file hash
+    const fileHash = crypto
+      .createHash("sha256")
+      .update(fs.readFileSync(filePath))
+      .digest("hex");
+    fileHashes.set(filePath, fileHash);
+
+    return true;
+  } catch (error) {
+    console.error(`❌ Error processing ${filePath}:`, error);
+    return false;
+  }
+}
+
+/**
+ * Sets up a watcher for migration files
+ * @param {Object} options - Configuration options
+ * @param {boolean} options.isDev - Whether to use development build settings
+ * @param {string[]} options.externalPackages - List of packages to keep external
+ * @param {Object} options.aliases - Map of import aliases
+ * @param {Object} options.onEvent - Optional callback for file change events
+ * @returns {Object} The file system watchers
+ */
+export function watchMigrationFiles(options = {}) {
+  // Initialize hash tracking for existing files
+  function initializeFileHashes(dir) {
+    if (!fs.existsSync(dir)) return;
+
+    const files = fs.readdirSync(dir);
+    for (const file of files) {
+      const filePath = join(dir, file);
+      if (fs.statSync(filePath).isDirectory()) {
+        initializeFileHashes(filePath);
+      } else if (
+        filePath.endsWith(".js") ||
+        filePath.endsWith(".yml") ||
+        filePath.endsWith(".sh") ||
+        filePath.endsWith(".ts")
+      ) {
+        try {
+          const fileHash = crypto
+            .createHash("sha256")
+            .update(fs.readFileSync(filePath))
+            .digest("hex");
+          fileHashes.set(filePath, fileHash);
+        } catch (error) {
+          console.error(`❌ Error hashing ${filePath}:`, error);
+        }
+      }
+    }
+  }
path: build-scripts/migration-handler.js
user: login: sourcery-ai[bot]
body: **issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
created_at: 2025-05-01T02:28:22Z
author_association: NONE
diff_hunk: @@ -0,0 +1,360 @@
+import crypto from "node:crypto";
+import fs from "node:fs";
+import { watch } from "node:fs";
+import { dirname, join, resolve } from "node:path";
+import { build } from "esbuild";
+import ora from "ora";
+
+// Store file hashes for detecting changes
+const fileHashes = new Map();
+
+/**
+ * Processes a single migration file that has changed
+ * @param {string} filePath - The path to the file that changed
+ * @param {boolean} isDev - Whether to use development build settings
+ * @param {string[]} externalPackages - List of packages to keep external
+ * @param {Object} aliases - Map of import aliases
+ * @returns {Promise<boolean>} - Whether the processing was successful
+ */
+export async function processMigrationFile(
+  filePath,
+  { isDev = false, externalPackages = [], aliases = {} },
+) {
+  try {
+    if (!fs.existsSync(filePath)) {
+      console.log(`❌ File no longer exists: ${filePath}`);
+      return false;
+    }
+
+    const fileSpinner = ora(`Processing ${filePath}...`).start();
+
+    // Calculate output path
+    const outputFile = filePath.replace("src/migrations", "dist/migrations");
+    const outputDir = dirname(outputFile);
+
+    // Ensure output directory exists
+    if (!fs.existsSync(outputDir)) {
+      fs.mkdirSync(outputDir, { recursive: true });
+    }
+
+    if (filePath.endsWith(".js")) {
+      // Process JS file - bundle it
+      const migrationBuildConfig = {
+        entryPoints: [filePath],
+        bundle: true,
+        platform: "node",
+        target: "node18",
+        outfile: outputFile,
+        format: "esm",
+        external: [...externalPackages, "node:*"],
+        packages: "external",
+        allowOverwrite: true,
+        alias: aliases,
+      };
+
+      if (isDev) {
+        migrationBuildConfig.minify = false;
+        migrationBuildConfig.sourcemap = true;
+        migrationBuildConfig.logLevel = "debug";
+        migrationBuildConfig.treeShaking = false;
+      } else {
+        migrationBuildConfig.minify = true;
+        migrationBuildConfig.sourcemap = false;
+        migrationBuildConfig.legalComments = "none";
+        migrationBuildConfig.treeShaking = true;
+      }
+
+      await build(migrationBuildConfig);
+    } else if (filePath.endsWith(".yml") || filePath.endsWith(".sh")) {
+      // Copy configuration file
+      fs.copyFileSync(filePath, outputFile);
+
+      // Set executable permissions on shell scripts
+      if (filePath.endsWith(".sh")) {
+        try {
+          fs.chmodSync(outputFile, 0o755);
+        } catch (chmodError) {
+          console.error(
+            `⚠️ Warning: Failed to set executable permissions on ${outputFile}:`,
+            chmodError,
+          );
+        }
+      }
+    }
+
+    fileSpinner.succeed(`Successfully processed ${filePath}`);
+
+    // Update the file hash
+    const fileHash = crypto
+      .createHash("sha256")
+      .update(fs.readFileSync(filePath))
+      .digest("hex");
+    fileHashes.set(filePath, fileHash);
+
+    return true;
+  } catch (error) {
+    console.error(`❌ Error processing ${filePath}:`, error);
+    return false;
+  }
+}
+
+/**
+ * Sets up a watcher for migration files
+ * @param {Object} options - Configuration options
+ * @param {boolean} options.isDev - Whether to use development build settings
+ * @param {string[]} options.externalPackages - List of packages to keep external
+ * @param {Object} options.aliases - Map of import aliases
+ * @param {Object} options.onEvent - Optional callback for file change events
+ * @returns {Object} The file system watchers
+ */
+export function watchMigrationFiles(options = {}) {
+  // Initialize hash tracking for existing files
+  function initializeFileHashes(dir) {
+    if (!fs.existsSync(dir)) return;
path: build-scripts/migration-handler.js
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Use block braces for ifs, whiles, etc. ([`use-braces`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/use-braces))

```suggestion
    if (!fs.existsSync(dir)) {
```

<br/><details><summary>Explanation</summary>It is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).
</details>
created_at: 2025-05-01T02:28:22Z
author_association: NONE
diff_hunk: @@ -0,0 +1,360 @@
+import crypto from "node:crypto";
+import fs from "node:fs";
+import { watch } from "node:fs";
+import { dirname, join, resolve } from "node:path";
+import { build } from "esbuild";
+import ora from "ora";
+
+// Store file hashes for detecting changes
+const fileHashes = new Map();
+
+/**
+ * Processes a single migration file that has changed
+ * @param {string} filePath - The path to the file that changed
+ * @param {boolean} isDev - Whether to use development build settings
+ * @param {string[]} externalPackages - List of packages to keep external
+ * @param {Object} aliases - Map of import aliases
+ * @returns {Promise<boolean>} - Whether the processing was successful
+ */
+export async function processMigrationFile(
+  filePath,
+  { isDev = false, externalPackages = [], aliases = {} },
+) {
+  try {
+    if (!fs.existsSync(filePath)) {
+      console.log(`❌ File no longer exists: ${filePath}`);
+      return false;
+    }
+
+    const fileSpinner = ora(`Processing ${filePath}...`).start();
+
+    // Calculate output path
+    const outputFile = filePath.replace("src/migrations", "dist/migrations");
+    const outputDir = dirname(outputFile);
+
+    // Ensure output directory exists
+    if (!fs.existsSync(outputDir)) {
+      fs.mkdirSync(outputDir, { recursive: true });
+    }
+
+    if (filePath.endsWith(".js")) {
+      // Process JS file - bundle it
+      const migrationBuildConfig = {
+        entryPoints: [filePath],
+        bundle: true,
+        platform: "node",
+        target: "node18",
+        outfile: outputFile,
+        format: "esm",
+        external: [...externalPackages, "node:*"],
+        packages: "external",
+        allowOverwrite: true,
+        alias: aliases,
+      };
+
+      if (isDev) {
+        migrationBuildConfig.minify = false;
+        migrationBuildConfig.sourcemap = true;
+        migrationBuildConfig.logLevel = "debug";
+        migrationBuildConfig.treeShaking = false;
+      } else {
+        migrationBuildConfig.minify = true;
+        migrationBuildConfig.sourcemap = false;
+        migrationBuildConfig.legalComments = "none";
+        migrationBuildConfig.treeShaking = true;
+      }
+
+      await build(migrationBuildConfig);
+    } else if (filePath.endsWith(".yml") || filePath.endsWith(".sh")) {
+      // Copy configuration file
+      fs.copyFileSync(filePath, outputFile);
+
+      // Set executable permissions on shell scripts
+      if (filePath.endsWith(".sh")) {
+        try {
+          fs.chmodSync(outputFile, 0o755);
+        } catch (chmodError) {
+          console.error(
+            `⚠️ Warning: Failed to set executable permissions on ${outputFile}:`,
+            chmodError,
+          );
+        }
+      }
+    }
+
+    fileSpinner.succeed(`Successfully processed ${filePath}`);
+
+    // Update the file hash
+    const fileHash = crypto
+      .createHash("sha256")
+      .update(fs.readFileSync(filePath))
+      .digest("hex");
+    fileHashes.set(filePath, fileHash);
+
+    return true;
+  } catch (error) {
+    console.error(`❌ Error processing ${filePath}:`, error);
+    return false;
+  }
+}
+
+/**
+ * Sets up a watcher for migration files
+ * @param {Object} options - Configuration options
+ * @param {boolean} options.isDev - Whether to use development build settings
+ * @param {string[]} options.externalPackages - List of packages to keep external
+ * @param {Object} options.aliases - Map of import aliases
+ * @param {Object} options.onEvent - Optional callback for file change events
+ * @returns {Object} The file system watchers
+ */
+export function watchMigrationFiles(options = {}) {
+  // Initialize hash tracking for existing files
+  function initializeFileHashes(dir) {
+    if (!fs.existsSync(dir)) return;
+
+    const files = fs.readdirSync(dir);
+    for (const file of files) {
+      const filePath = join(dir, file);
+      if (fs.statSync(filePath).isDirectory()) {
+        initializeFileHashes(filePath);
+      } else if (
+        filePath.endsWith(".js") ||
+        filePath.endsWith(".yml") ||
+        filePath.endsWith(".sh") ||
+        filePath.endsWith(".ts")
+      ) {
+        try {
+          const fileHash = crypto
+            .createHash("sha256")
+            .update(fs.readFileSync(filePath))
+            .digest("hex");
+          fileHashes.set(filePath, fileHash);
+        } catch (error) {
+          console.error(`❌ Error hashing ${filePath}:`, error);
+        }
+      }
+    }
+  }
+
+  // Initialize hashes for existing files
+  initializeFileHashes("src/migrations");
+  initializeFileHashes("src");
+
+  const watchers = {
+    // Set up the watcher for migrations
+    migrations: watch(
+      "src/migrations",
+      { recursive: true },
+      async (eventType, filename) => {
+        if (!filename) return;
path: build-scripts/migration-handler.js
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Use block braces for ifs, whiles, etc. ([`use-braces`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/use-braces))

```suggestion
        if (!filename) {
```

<br/><details><summary>Explanation</summary>It is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).
</details>
created_at: 2025-05-01T02:28:22Z
author_association: NONE
diff_hunk: @@ -0,0 +1,360 @@
+import crypto from "node:crypto";
+import fs from "node:fs";
+import { watch } from "node:fs";
+import { dirname, join, resolve } from "node:path";
+import { build } from "esbuild";
+import ora from "ora";
+
+// Store file hashes for detecting changes
+const fileHashes = new Map();
+
+/**
+ * Processes a single migration file that has changed
+ * @param {string} filePath - The path to the file that changed
+ * @param {boolean} isDev - Whether to use development build settings
+ * @param {string[]} externalPackages - List of packages to keep external
+ * @param {Object} aliases - Map of import aliases
+ * @returns {Promise<boolean>} - Whether the processing was successful
+ */
+export async function processMigrationFile(
+  filePath,
+  { isDev = false, externalPackages = [], aliases = {} },
+) {
+  try {
+    if (!fs.existsSync(filePath)) {
+      console.log(`❌ File no longer exists: ${filePath}`);
+      return false;
+    }
+
+    const fileSpinner = ora(`Processing ${filePath}...`).start();
+
+    // Calculate output path
+    const outputFile = filePath.replace("src/migrations", "dist/migrations");
+    const outputDir = dirname(outputFile);
+
+    // Ensure output directory exists
+    if (!fs.existsSync(outputDir)) {
+      fs.mkdirSync(outputDir, { recursive: true });
+    }
+
+    if (filePath.endsWith(".js")) {
+      // Process JS file - bundle it
+      const migrationBuildConfig = {
+        entryPoints: [filePath],
+        bundle: true,
+        platform: "node",
+        target: "node18",
+        outfile: outputFile,
+        format: "esm",
+        external: [...externalPackages, "node:*"],
+        packages: "external",
+        allowOverwrite: true,
+        alias: aliases,
+      };
+
+      if (isDev) {
+        migrationBuildConfig.minify = false;
+        migrationBuildConfig.sourcemap = true;
+        migrationBuildConfig.logLevel = "debug";
+        migrationBuildConfig.treeShaking = false;
+      } else {
+        migrationBuildConfig.minify = true;
+        migrationBuildConfig.sourcemap = false;
+        migrationBuildConfig.legalComments = "none";
+        migrationBuildConfig.treeShaking = true;
+      }
+
+      await build(migrationBuildConfig);
+    } else if (filePath.endsWith(".yml") || filePath.endsWith(".sh")) {
+      // Copy configuration file
+      fs.copyFileSync(filePath, outputFile);
+
+      // Set executable permissions on shell scripts
+      if (filePath.endsWith(".sh")) {
+        try {
+          fs.chmodSync(outputFile, 0o755);
+        } catch (chmodError) {
+          console.error(
+            `⚠️ Warning: Failed to set executable permissions on ${outputFile}:`,
+            chmodError,
+          );
+        }
+      }
+    }
+
+    fileSpinner.succeed(`Successfully processed ${filePath}`);
+
+    // Update the file hash
+    const fileHash = crypto
+      .createHash("sha256")
+      .update(fs.readFileSync(filePath))
+      .digest("hex");
+    fileHashes.set(filePath, fileHash);
+
+    return true;
+  } catch (error) {
+    console.error(`❌ Error processing ${filePath}:`, error);
+    return false;
+  }
+}
+
+/**
+ * Sets up a watcher for migration files
+ * @param {Object} options - Configuration options
+ * @param {boolean} options.isDev - Whether to use development build settings
+ * @param {string[]} options.externalPackages - List of packages to keep external
+ * @param {Object} options.aliases - Map of import aliases
+ * @param {Object} options.onEvent - Optional callback for file change events
+ * @returns {Object} The file system watchers
+ */
+export function watchMigrationFiles(options = {}) {
+  // Initialize hash tracking for existing files
+  function initializeFileHashes(dir) {
+    if (!fs.existsSync(dir)) return;
+
+    const files = fs.readdirSync(dir);
+    for (const file of files) {
+      const filePath = join(dir, file);
+      if (fs.statSync(filePath).isDirectory()) {
+        initializeFileHashes(filePath);
+      } else if (
+        filePath.endsWith(".js") ||
+        filePath.endsWith(".yml") ||
+        filePath.endsWith(".sh") ||
+        filePath.endsWith(".ts")
+      ) {
+        try {
+          const fileHash = crypto
+            .createHash("sha256")
+            .update(fs.readFileSync(filePath))
+            .digest("hex");
+          fileHashes.set(filePath, fileHash);
+        } catch (error) {
+          console.error(`❌ Error hashing ${filePath}:`, error);
+        }
+      }
+    }
+  }
+
+  // Initialize hashes for existing files
+  initializeFileHashes("src/migrations");
+  initializeFileHashes("src");
+
+  const watchers = {
+    // Set up the watcher for migrations
+    migrations: watch(
+      "src/migrations",
+      { recursive: true },
+      async (eventType, filename) => {
+        if (!filename) return;
+
+        const filePath = join("src/migrations", filename);
+
+        // Skip if file doesn't exist (might have been deleted)
+        if (!fs.existsSync(filePath)) {
+          console.log(`❌ File no longer exists: ${filePath}`);
+          return;
+        }
+
+        // Skip if not a file type we care about
+        if (
+          !filePath.endsWith(".js") &&
+          !filePath.endsWith(".yml") &&
+          !filePath.endsWith(".sh")
+        ) {
+          return;
+        }
+
+        // Check if content actually changed by comparing hashes
+        try {
+          const currentHash = crypto
+            .createHash("sha256")
+            .update(fs.readFileSync(filePath))
+            .digest("hex");
+
+          const previousHash = fileHashes.get(filePath);
+
+          // Only process if file is new or contents changed
+          if (previousHash !== currentHash) {
+            console.log(`🔄 Detected change in ${filename}`);
+            await processMigrationFile(filePath, options);
+          }
+        } catch (error) {
+          console.error(`❌ Error checking file ${filePath}:`, error);
+        }
+      },
+    ),
+
+    // Set up the watcher for the entire src directory
+    src: watch("src", { recursive: true }, async (eventType, filename) => {
+      if (!filename) return;
path: build-scripts/migration-handler.js
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Use block braces for ifs, whiles, etc. ([`use-braces`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/use-braces))

```suggestion
      if (!filename) {
```

<br/><details><summary>Explanation</summary>It is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).
</details>
created_at: 2025-05-01T02:28:22Z
author_association: NONE
diff_hunk: @@ -0,0 +1,360 @@
+import crypto from "node:crypto";
+import fs from "node:fs";
+import { watch } from "node:fs";
+import { dirname, join, resolve } from "node:path";
+import { build } from "esbuild";
+import ora from "ora";
+
+// Store file hashes for detecting changes
+const fileHashes = new Map();
+
+/**
+ * Processes a single migration file that has changed
+ * @param {string} filePath - The path to the file that changed
+ * @param {boolean} isDev - Whether to use development build settings
+ * @param {string[]} externalPackages - List of packages to keep external
+ * @param {Object} aliases - Map of import aliases
+ * @returns {Promise<boolean>} - Whether the processing was successful
+ */
+export async function processMigrationFile(
+  filePath,
+  { isDev = false, externalPackages = [], aliases = {} },
+) {
+  try {
+    if (!fs.existsSync(filePath)) {
+      console.log(`❌ File no longer exists: ${filePath}`);
+      return false;
+    }
+
+    const fileSpinner = ora(`Processing ${filePath}...`).start();
+
+    // Calculate output path
+    const outputFile = filePath.replace("src/migrations", "dist/migrations");
+    const outputDir = dirname(outputFile);
+
+    // Ensure output directory exists
+    if (!fs.existsSync(outputDir)) {
+      fs.mkdirSync(outputDir, { recursive: true });
+    }
+
+    if (filePath.endsWith(".js")) {
+      // Process JS file - bundle it
+      const migrationBuildConfig = {
+        entryPoints: [filePath],
+        bundle: true,
+        platform: "node",
+        target: "node18",
+        outfile: outputFile,
+        format: "esm",
+        external: [...externalPackages, "node:*"],
+        packages: "external",
+        allowOverwrite: true,
+        alias: aliases,
+      };
+
+      if (isDev) {
+        migrationBuildConfig.minify = false;
+        migrationBuildConfig.sourcemap = true;
+        migrationBuildConfig.logLevel = "debug";
+        migrationBuildConfig.treeShaking = false;
+      } else {
+        migrationBuildConfig.minify = true;
+        migrationBuildConfig.sourcemap = false;
+        migrationBuildConfig.legalComments = "none";
+        migrationBuildConfig.treeShaking = true;
+      }
+
+      await build(migrationBuildConfig);
+    } else if (filePath.endsWith(".yml") || filePath.endsWith(".sh")) {
+      // Copy configuration file
+      fs.copyFileSync(filePath, outputFile);
+
+      // Set executable permissions on shell scripts
+      if (filePath.endsWith(".sh")) {
+        try {
+          fs.chmodSync(outputFile, 0o755);
+        } catch (chmodError) {
+          console.error(
+            `⚠️ Warning: Failed to set executable permissions on ${outputFile}:`,
+            chmodError,
+          );
+        }
+      }
+    }
+
+    fileSpinner.succeed(`Successfully processed ${filePath}`);
+
+    // Update the file hash
+    const fileHash = crypto
+      .createHash("sha256")
+      .update(fs.readFileSync(filePath))
+      .digest("hex");
+    fileHashes.set(filePath, fileHash);
+
+    return true;
+  } catch (error) {
+    console.error(`❌ Error processing ${filePath}:`, error);
+    return false;
+  }
+}
+
+/**
+ * Sets up a watcher for migration files
+ * @param {Object} options - Configuration options
+ * @param {boolean} options.isDev - Whether to use development build settings
+ * @param {string[]} options.externalPackages - List of packages to keep external
+ * @param {Object} options.aliases - Map of import aliases
+ * @param {Object} options.onEvent - Optional callback for file change events
+ * @returns {Object} The file system watchers
+ */
+export function watchMigrationFiles(options = {}) {
+  // Initialize hash tracking for existing files
+  function initializeFileHashes(dir) {
+    if (!fs.existsSync(dir)) return;
+
+    const files = fs.readdirSync(dir);
+    for (const file of files) {
+      const filePath = join(dir, file);
+      if (fs.statSync(filePath).isDirectory()) {
+        initializeFileHashes(filePath);
+      } else if (
+        filePath.endsWith(".js") ||
+        filePath.endsWith(".yml") ||
+        filePath.endsWith(".sh") ||
+        filePath.endsWith(".ts")
+      ) {
+        try {
+          const fileHash = crypto
+            .createHash("sha256")
+            .update(fs.readFileSync(filePath))
+            .digest("hex");
+          fileHashes.set(filePath, fileHash);
+        } catch (error) {
+          console.error(`❌ Error hashing ${filePath}:`, error);
+        }
+      }
+    }
+  }
+
+  // Initialize hashes for existing files
+  initializeFileHashes("src/migrations");
+  initializeFileHashes("src");
+
+  const watchers = {
+    // Set up the watcher for migrations
+    migrations: watch(
+      "src/migrations",
+      { recursive: true },
+      async (eventType, filename) => {
+        if (!filename) return;
+
+        const filePath = join("src/migrations", filename);
+
+        // Skip if file doesn't exist (might have been deleted)
+        if (!fs.existsSync(filePath)) {
+          console.log(`❌ File no longer exists: ${filePath}`);
+          return;
+        }
+
+        // Skip if not a file type we care about
+        if (
+          !filePath.endsWith(".js") &&
+          !filePath.endsWith(".yml") &&
+          !filePath.endsWith(".sh")
+        ) {
+          return;
+        }
+
+        // Check if content actually changed by comparing hashes
+        try {
+          const currentHash = crypto
+            .createHash("sha256")
+            .update(fs.readFileSync(filePath))
+            .digest("hex");
+
+          const previousHash = fileHashes.get(filePath);
+
+          // Only process if file is new or contents changed
+          if (previousHash !== currentHash) {
+            console.log(`🔄 Detected change in ${filename}`);
+            await processMigrationFile(filePath, options);
+          }
+        } catch (error) {
+          console.error(`❌ Error checking file ${filePath}:`, error);
+        }
+      },
+    ),
+
+    // Set up the watcher for the entire src directory
+    src: watch("src", { recursive: true }, async (eventType, filename) => {
+      if (!filename) return;
+
+      // Skip migrations directory as it's handled by the other watcher
+      if (filename.startsWith("migrations/")) return;
path: build-scripts/migration-handler.js
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Use block braces for ifs, whiles, etc. ([`use-braces`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/use-braces))

```suggestion
      if (filename.startsWith("migrations/")) {
```

<br/><details><summary>Explanation</summary>It is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).
</details>
created_at: 2025-05-01T02:28:22Z
author_association: NONE
diff_hunk: @@ -0,0 +1,360 @@
+import crypto from "node:crypto";
+import fs from "node:fs";
+import { watch } from "node:fs";
+import { dirname, join, resolve } from "node:path";
+import { build } from "esbuild";
+import ora from "ora";
+
+// Store file hashes for detecting changes
+const fileHashes = new Map();
+
+/**
+ * Processes a single migration file that has changed
+ * @param {string} filePath - The path to the file that changed
+ * @param {boolean} isDev - Whether to use development build settings
+ * @param {string[]} externalPackages - List of packages to keep external
+ * @param {Object} aliases - Map of import aliases
+ * @returns {Promise<boolean>} - Whether the processing was successful
+ */
+export async function processMigrationFile(
+  filePath,
+  { isDev = false, externalPackages = [], aliases = {} },
+) {
+  try {
+    if (!fs.existsSync(filePath)) {
+      console.log(`❌ File no longer exists: ${filePath}`);
+      return false;
+    }
+
+    const fileSpinner = ora(`Processing ${filePath}...`).start();
+
+    // Calculate output path
+    const outputFile = filePath.replace("src/migrations", "dist/migrations");
+    const outputDir = dirname(outputFile);
+
+    // Ensure output directory exists
+    if (!fs.existsSync(outputDir)) {
+      fs.mkdirSync(outputDir, { recursive: true });
+    }
+
+    if (filePath.endsWith(".js")) {
+      // Process JS file - bundle it
+      const migrationBuildConfig = {
+        entryPoints: [filePath],
+        bundle: true,
+        platform: "node",
+        target: "node18",
+        outfile: outputFile,
+        format: "esm",
+        external: [...externalPackages, "node:*"],
+        packages: "external",
+        allowOverwrite: true,
+        alias: aliases,
+      };
+
+      if (isDev) {
+        migrationBuildConfig.minify = false;
+        migrationBuildConfig.sourcemap = true;
+        migrationBuildConfig.logLevel = "debug";
+        migrationBuildConfig.treeShaking = false;
+      } else {
+        migrationBuildConfig.minify = true;
+        migrationBuildConfig.sourcemap = false;
+        migrationBuildConfig.legalComments = "none";
+        migrationBuildConfig.treeShaking = true;
+      }
+
+      await build(migrationBuildConfig);
+    } else if (filePath.endsWith(".yml") || filePath.endsWith(".sh")) {
+      // Copy configuration file
+      fs.copyFileSync(filePath, outputFile);
+
+      // Set executable permissions on shell scripts
+      if (filePath.endsWith(".sh")) {
+        try {
+          fs.chmodSync(outputFile, 0o755);
+        } catch (chmodError) {
+          console.error(
+            `⚠️ Warning: Failed to set executable permissions on ${outputFile}:`,
+            chmodError,
+          );
+        }
+      }
+    }
+
+    fileSpinner.succeed(`Successfully processed ${filePath}`);
+
+    // Update the file hash
+    const fileHash = crypto
+      .createHash("sha256")
+      .update(fs.readFileSync(filePath))
+      .digest("hex");
+    fileHashes.set(filePath, fileHash);
+
+    return true;
+  } catch (error) {
+    console.error(`❌ Error processing ${filePath}:`, error);
+    return false;
+  }
+}
+
+/**
+ * Sets up a watcher for migration files
+ * @param {Object} options - Configuration options
+ * @param {boolean} options.isDev - Whether to use development build settings
+ * @param {string[]} options.externalPackages - List of packages to keep external
+ * @param {Object} options.aliases - Map of import aliases
+ * @param {Object} options.onEvent - Optional callback for file change events
+ * @returns {Object} The file system watchers
+ */
+export function watchMigrationFiles(options = {}) {
+  // Initialize hash tracking for existing files
+  function initializeFileHashes(dir) {
+    if (!fs.existsSync(dir)) return;
+
+    const files = fs.readdirSync(dir);
+    for (const file of files) {
+      const filePath = join(dir, file);
+      if (fs.statSync(filePath).isDirectory()) {
+        initializeFileHashes(filePath);
+      } else if (
+        filePath.endsWith(".js") ||
+        filePath.endsWith(".yml") ||
+        filePath.endsWith(".sh") ||
+        filePath.endsWith(".ts")
+      ) {
+        try {
+          const fileHash = crypto
+            .createHash("sha256")
+            .update(fs.readFileSync(filePath))
+            .digest("hex");
+          fileHashes.set(filePath, fileHash);
+        } catch (error) {
+          console.error(`❌ Error hashing ${filePath}:`, error);
+        }
+      }
+    }
+  }
+
+  // Initialize hashes for existing files
+  initializeFileHashes("src/migrations");
+  initializeFileHashes("src");
+
+  const watchers = {
+    // Set up the watcher for migrations
+    migrations: watch(
+      "src/migrations",
+      { recursive: true },
+      async (eventType, filename) => {
+        if (!filename) return;
+
+        const filePath = join("src/migrations", filename);
+
+        // Skip if file doesn't exist (might have been deleted)
+        if (!fs.existsSync(filePath)) {
+          console.log(`❌ File no longer exists: ${filePath}`);
+          return;
+        }
+
+        // Skip if not a file type we care about
+        if (
+          !filePath.endsWith(".js") &&
+          !filePath.endsWith(".yml") &&
+          !filePath.endsWith(".sh")
+        ) {
+          return;
+        }
+
+        // Check if content actually changed by comparing hashes
+        try {
+          const currentHash = crypto
+            .createHash("sha256")
+            .update(fs.readFileSync(filePath))
+            .digest("hex");
+
+          const previousHash = fileHashes.get(filePath);
+
+          // Only process if file is new or contents changed
+          if (previousHash !== currentHash) {
+            console.log(`🔄 Detected change in ${filename}`);
+            await processMigrationFile(filePath, options);
+          }
+        } catch (error) {
+          console.error(`❌ Error checking file ${filePath}:`, error);
+        }
+      },
+    ),
+
+    // Set up the watcher for the entire src directory
+    src: watch("src", { recursive: true }, async (eventType, filename) => {
+      if (!filename) return;
+
+      // Skip migrations directory as it's handled by the other watcher
+      if (filename.startsWith("migrations/")) return;
+
+      const filePath = join("src", filename);
+
+      // Skip if file doesn't exist (might have been deleted)
+      if (!fs.existsSync(filePath)) {
+        console.log(`❌ File no longer exists: ${filePath}`);
+        return;
+      }
+
+      // Skip if not a file type we care about
+      if (
+        !filePath.endsWith(".js") &&
+        !filePath.endsWith(".ts") &&
+        !filePath.endsWith(".mjs") &&
+        !filePath.endsWith(".cjs")
+      ) {
+        return;
+      }
+
+      try {
+        const currentHash = crypto
+          .createHash("sha256")
+          .update(fs.readFileSync(filePath))
+          .digest("hex");
+
+        const previousHash = fileHashes.get(filePath);
+
+        // Only process if file is new or contents changed
+        if (previousHash !== currentHash) {
+          console.log(`🔄 Detected change in ${filename}`);
+          fileHashes.set(filePath, currentHash);
+
+          // Trigger a rebuild of the main bundle
+          if (typeof options.onEvent === "function") {
+            options.onEvent({
+              type: "change",
+              path: filePath,
+              requiresRebuild: true,
+            });
+          }
+        }
+      } catch (error) {
+        console.error(`❌ Error checking file ${filePath}:`, error);
+        if (typeof options.onEvent === "function") {
+          options.onEvent({ error });
+        }
+      }
+    }),
+  };
+
+  // Return an object with both watchers
+  return {
+    close: () => {
+      watchers.migrations.close();
+      watchers.src.close();
+    },
+  };
+}
+
+/**
+ * Process all migration files
+ * @param {Object} options - Configuration options
+ * @param {boolean} options.isDev - Whether to use development build settings
+ * @param {string[]} options.externalPackages - List of packages to keep external
+ * @param {Object} options.aliases - Map of import aliases
+ */
+export async function processMigrations(options = {}) {
+  try {
+    // Step 1: Process migration JS files to inline their dependencies
+    const migrationJsFiles = [];
+
+    /**
+     * Recursively finds all JavaScript files in a given directory.
+     * @param {string} dir - The directory to search for JavaScript files.
+     */
+    function findJsFiles(dir) {
+      if (!fs.existsSync(dir)) {
+        return;
+      }
+      const files = fs.readdirSync(dir);
+      for (const file of files) {
+        const filePath = join(dir, file);
+        if (fs.statSync(filePath).isDirectory()) {
+          findJsFiles(filePath);
+        } else if (file.endsWith(".js")) {
+          migrationJsFiles.push(filePath);
+        }
+      }
+    }
path: build-scripts/migration-handler.js
user: login: sourcery-ai[bot]
body: **issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
created_at: 2025-05-01T02:28:22Z
author_association: NONE
diff_hunk: @@ -0,0 +1,360 @@
+import crypto from "node:crypto";
+import fs from "node:fs";
+import { watch } from "node:fs";
+import { dirname, join, resolve } from "node:path";
+import { build } from "esbuild";
+import ora from "ora";
+
+// Store file hashes for detecting changes
+const fileHashes = new Map();
+
+/**
+ * Processes a single migration file that has changed
+ * @param {string} filePath - The path to the file that changed
+ * @param {boolean} isDev - Whether to use development build settings
+ * @param {string[]} externalPackages - List of packages to keep external
+ * @param {Object} aliases - Map of import aliases
+ * @returns {Promise<boolean>} - Whether the processing was successful
+ */
+export async function processMigrationFile(
+  filePath,
+  { isDev = false, externalPackages = [], aliases = {} },
+) {
+  try {
+    if (!fs.existsSync(filePath)) {
+      console.log(`❌ File no longer exists: ${filePath}`);
+      return false;
+    }
+
+    const fileSpinner = ora(`Processing ${filePath}...`).start();
+
+    // Calculate output path
+    const outputFile = filePath.replace("src/migrations", "dist/migrations");
+    const outputDir = dirname(outputFile);
+
+    // Ensure output directory exists
+    if (!fs.existsSync(outputDir)) {
+      fs.mkdirSync(outputDir, { recursive: true });
+    }
+
+    if (filePath.endsWith(".js")) {
+      // Process JS file - bundle it
+      const migrationBuildConfig = {
+        entryPoints: [filePath],
+        bundle: true,
+        platform: "node",
+        target: "node18",
+        outfile: outputFile,
+        format: "esm",
+        external: [...externalPackages, "node:*"],
+        packages: "external",
+        allowOverwrite: true,
+        alias: aliases,
+      };
+
+      if (isDev) {
+        migrationBuildConfig.minify = false;
+        migrationBuildConfig.sourcemap = true;
+        migrationBuildConfig.logLevel = "debug";
+        migrationBuildConfig.treeShaking = false;
+      } else {
+        migrationBuildConfig.minify = true;
+        migrationBuildConfig.sourcemap = false;
+        migrationBuildConfig.legalComments = "none";
+        migrationBuildConfig.treeShaking = true;
+      }
+
+      await build(migrationBuildConfig);
+    } else if (filePath.endsWith(".yml") || filePath.endsWith(".sh")) {
+      // Copy configuration file
+      fs.copyFileSync(filePath, outputFile);
+
+      // Set executable permissions on shell scripts
+      if (filePath.endsWith(".sh")) {
+        try {
+          fs.chmodSync(outputFile, 0o755);
+        } catch (chmodError) {
+          console.error(
+            `⚠️ Warning: Failed to set executable permissions on ${outputFile}:`,
+            chmodError,
+          );
+        }
+      }
+    }
+
+    fileSpinner.succeed(`Successfully processed ${filePath}`);
+
+    // Update the file hash
+    const fileHash = crypto
+      .createHash("sha256")
+      .update(fs.readFileSync(filePath))
+      .digest("hex");
+    fileHashes.set(filePath, fileHash);
+
+    return true;
+  } catch (error) {
+    console.error(`❌ Error processing ${filePath}:`, error);
+    return false;
+  }
+}
+
+/**
+ * Sets up a watcher for migration files
+ * @param {Object} options - Configuration options
+ * @param {boolean} options.isDev - Whether to use development build settings
+ * @param {string[]} options.externalPackages - List of packages to keep external
+ * @param {Object} options.aliases - Map of import aliases
+ * @param {Object} options.onEvent - Optional callback for file change events
+ * @returns {Object} The file system watchers
+ */
+export function watchMigrationFiles(options = {}) {
+  // Initialize hash tracking for existing files
+  function initializeFileHashes(dir) {
+    if (!fs.existsSync(dir)) return;
+
+    const files = fs.readdirSync(dir);
+    for (const file of files) {
+      const filePath = join(dir, file);
+      if (fs.statSync(filePath).isDirectory()) {
+        initializeFileHashes(filePath);
+      } else if (
+        filePath.endsWith(".js") ||
+        filePath.endsWith(".yml") ||
+        filePath.endsWith(".sh") ||
+        filePath.endsWith(".ts")
+      ) {
+        try {
+          const fileHash = crypto
+            .createHash("sha256")
+            .update(fs.readFileSync(filePath))
+            .digest("hex");
+          fileHashes.set(filePath, fileHash);
+        } catch (error) {
+          console.error(`❌ Error hashing ${filePath}:`, error);
+        }
+      }
+    }
+  }
+
+  // Initialize hashes for existing files
+  initializeFileHashes("src/migrations");
+  initializeFileHashes("src");
+
+  const watchers = {
+    // Set up the watcher for migrations
+    migrations: watch(
+      "src/migrations",
+      { recursive: true },
+      async (eventType, filename) => {
+        if (!filename) return;
+
+        const filePath = join("src/migrations", filename);
+
+        // Skip if file doesn't exist (might have been deleted)
+        if (!fs.existsSync(filePath)) {
+          console.log(`❌ File no longer exists: ${filePath}`);
+          return;
+        }
+
+        // Skip if not a file type we care about
+        if (
+          !filePath.endsWith(".js") &&
+          !filePath.endsWith(".yml") &&
+          !filePath.endsWith(".sh")
+        ) {
+          return;
+        }
+
+        // Check if content actually changed by comparing hashes
+        try {
+          const currentHash = crypto
+            .createHash("sha256")
+            .update(fs.readFileSync(filePath))
+            .digest("hex");
+
+          const previousHash = fileHashes.get(filePath);
+
+          // Only process if file is new or contents changed
+          if (previousHash !== currentHash) {
+            console.log(`🔄 Detected change in ${filename}`);
+            await processMigrationFile(filePath, options);
+          }
+        } catch (error) {
+          console.error(`❌ Error checking file ${filePath}:`, error);
+        }
+      },
+    ),
+
+    // Set up the watcher for the entire src directory
+    src: watch("src", { recursive: true }, async (eventType, filename) => {
+      if (!filename) return;
+
+      // Skip migrations directory as it's handled by the other watcher
+      if (filename.startsWith("migrations/")) return;
+
+      const filePath = join("src", filename);
+
+      // Skip if file doesn't exist (might have been deleted)
+      if (!fs.existsSync(filePath)) {
+        console.log(`❌ File no longer exists: ${filePath}`);
+        return;
+      }
+
+      // Skip if not a file type we care about
+      if (
+        !filePath.endsWith(".js") &&
+        !filePath.endsWith(".ts") &&
+        !filePath.endsWith(".mjs") &&
+        !filePath.endsWith(".cjs")
+      ) {
+        return;
+      }
+
+      try {
+        const currentHash = crypto
+          .createHash("sha256")
+          .update(fs.readFileSync(filePath))
+          .digest("hex");
+
+        const previousHash = fileHashes.get(filePath);
+
+        // Only process if file is new or contents changed
+        if (previousHash !== currentHash) {
+          console.log(`🔄 Detected change in ${filename}`);
+          fileHashes.set(filePath, currentHash);
+
+          // Trigger a rebuild of the main bundle
+          if (typeof options.onEvent === "function") {
+            options.onEvent({
+              type: "change",
+              path: filePath,
+              requiresRebuild: true,
+            });
+          }
+        }
+      } catch (error) {
+        console.error(`❌ Error checking file ${filePath}:`, error);
+        if (typeof options.onEvent === "function") {
+          options.onEvent({ error });
+        }
+      }
+    }),
+  };
+
+  // Return an object with both watchers
+  return {
+    close: () => {
+      watchers.migrations.close();
+      watchers.src.close();
+    },
+  };
+}
+
+/**
+ * Process all migration files
+ * @param {Object} options - Configuration options
+ * @param {boolean} options.isDev - Whether to use development build settings
+ * @param {string[]} options.externalPackages - List of packages to keep external
+ * @param {Object} options.aliases - Map of import aliases
+ */
+export async function processMigrations(options = {}) {
+  try {
+    // Step 1: Process migration JS files to inline their dependencies
+    const migrationJsFiles = [];
+
+    /**
+     * Recursively finds all JavaScript files in a given directory.
+     * @param {string} dir - The directory to search for JavaScript files.
+     */
+    function findJsFiles(dir) {
+      if (!fs.existsSync(dir)) {
+        return;
+      }
+      const files = fs.readdirSync(dir);
+      for (const file of files) {
+        const filePath = join(dir, file);
+        if (fs.statSync(filePath).isDirectory()) {
+          findJsFiles(filePath);
+        } else if (file.endsWith(".js")) {
+          migrationJsFiles.push(filePath);
+        }
+      }
+    }
+
+    findJsFiles("src/migrations");
+
+    // Process each migration JS file
+    const migrationSpinner = ora(
+      `Processing ${migrationJsFiles.length} migration JS files...`,
+    ).start();
+
+    for (const migrationFile of migrationJsFiles) {
+      await processMigrationFile(migrationFile, options);
+    }
+
+    migrationSpinner.succeed(
+      `Processed ${migrationJsFiles.length} migration JS files.`,
+    );
+
+    // Step 2: Copy migration .yml and .sh files
+    const copySpinner = ora(
+      "Copying migration configuration files (.yml, .sh)...",
+    ).start();
+
+    /**
+     * Recursively finds and copies all .yml and .sh files from a source directory to a destination directory.
+     * @param {string} srcDir - The source directory to search in.
+     * @param {string} destDir - The destination directory to copy to.
+     */
+    function copyMigrationConfigFiles(srcDir, destDir) {
+      if (!fs.existsSync(srcDir)) {
+        return 0;
+      }
+
+      let copiedCount = 0;
+      const items = fs.readdirSync(srcDir);
+
+      for (const item of items) {
+        const srcPath = join(srcDir, item);
+        const destPath = join(destDir, item);
+
+        if (fs.statSync(srcPath).isDirectory()) {
+          if (!fs.existsSync(destPath)) {
+            fs.mkdirSync(destPath, { recursive: true });
+          }
+          copiedCount += copyMigrationConfigFiles(srcPath, destPath);
+        } else if (item.endsWith(".yml") || item.endsWith(".sh")) {
+          // Always copy the file regardless if it exists or has changed
+          fs.copyFileSync(srcPath, destPath);
+
+          // Set executable permissions for shell scripts
+          if (item.endsWith(".sh")) {
+            try {
+              fs.chmodSync(destPath, 0o755);
+            } catch (chmodError) {
+              console.error(
+                `⚠️ Warning: Failed to set executable permissions on ${destPath}:`,
+                chmodError,
+              );
+            }
+          }
+
+          copiedCount++;
+        }
+      }
+
+      return copiedCount;
+    }
path: build-scripts/migration-handler.js
user: login: sourcery-ai[bot]
body: **issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/JavaScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
created_at: 2025-05-01T02:28:22Z
author_association: NONE

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-05-01T02:26:52Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide

This pull request refactors the `esbuild.config.js` build script by extracting core logic for configuration, migration handling, and file watching into separate modules within a new `build-scripts` directory. It introduces a `--watch` mode for development, leveraging content hashing for efficient rebuilds, and updates the shell utility for better output streaming.

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Modularized build configuration and migration handling logic. | <ul><li>Extracted build configuration generation (external packages, aliases, dev/prod settings) into `build-config.js`.</li><li>Extracted migration file processing (JS bundling, YML/SH copying, permissions) into `migration-handler.js`.</li><li>Simplified `esbuild.config.js` to import and orchestrate the new modules.</li><li>Removed obsolete `migration` directory paths from `tsconfig.json` includes.</li></ul> | `esbuild.config.js`<br/>`build-scripts/build-config.js`<br/>`build-scripts/migration-handler.js`<br/>`tsconfig.json` |
| Implemented a new file watching mode with content hashing. | <ul><li>Added `--watch` flag support parsed via `parseArgs` in `build-config.js`.</li><li>Introduced `watchMigrationFiles` in `migration-handler.js` using `fs.watch`.</li><li>Implemented file content hashing to detect actual changes and avoid unnecessary rebuilds.</li><li>Integrated watcher into `esbuild.config.js` to trigger migration processing or full rebuilds.</li><li>Updated `dev` script in `package.json` to use the `--watch` flag.</li></ul> | `esbuild.config.js`<br/>`build-scripts/migration-handler.js`<br/>`package.json` |
| Improved shell command execution output. | <ul><li>Modified `shell` utility to stream stdout and stderr directly to the console.</li><li>Reduced spinner usage to only indicate start/success/failure status.</li></ul> | `src/utils/shell.js` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: NONE
user: login: jason-capsule42
created_at: 2025-05-02T17:32:21Z
body: :tada: This PR is included in version 2.1.0-rc-59.2 :tada:

The release is available on:
- [npm package (@rc dist-tag)](https://www.npmjs.com/package/@aurodesignsystem/auro-cli/v/2.1.0-rc-59.2)
- [GitHub release](https://github.com/AlaskaAirlines/auro-cli/releases/tag/v2.1.0-rc-59.2)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: MEMBER
user: login: jason-capsule42
created_at: 2025-05-02T18:20:24Z
body: :tada: This PR is included in version 2.2.0-rc-59.1 :tada:

The release is available on:
- [npm package (@rc dist-tag)](https://www.npmjs.com/package/@aurodesignsystem/auro-cli/v/2.2.0-rc-59.1)
- [GitHub release](https://github.com/AlaskaAirlines/auro-cli/releases/tag/v2.2.0-rc-59.1)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: MEMBER
user: login: jason-capsule42
created_at: 2025-05-02T19:14:39Z
body: :tada: This PR is included in version 2.2.0-rc-69.1 :tada:

The release is available on:
- [npm package (@rc dist-tag)](https://www.npmjs.com/package/@aurodesignsystem/auro-cli/v/2.2.0-rc-69.1)
- [GitHub release](https://github.com/AlaskaAirlines/auro-cli/releases/tag/v2.2.0-rc-69.1)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: MEMBER
user: login: jason-capsule42
created_at: 2025-05-02T19:18:45Z
body: :tada: This PR is included in version 2.2.0 :tada:

The release is available on:
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@aurodesignsystem/auro-cli/v/2.2.0)
- [GitHub release](https://github.com/AlaskaAirlines/auro-cli/releases/tag/v2.2.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: MEMBER
user: login: jason-capsule42
created_at: 2025-05-14T23:08:43Z
body: :tada: This PR is included in version 2.2.0-rc-80.1 :tada:

The release is available on:
- [npm package (@rc dist-tag)](https://www.npmjs.com/package/@aurodesignsystem/auro-cli/v/2.2.0-rc-80.1)
- [GitHub release](https://github.com/AlaskaAirlines/auro-cli/releases/tag/v2.2.0-rc-80.1)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: MEMBER