diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b26375a..0a6d800 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -49,6 +49,9 @@ "version": "0.1.0" }, { + "name": "core", + "source": "./claude/core", + "description": "Laravel migration helpers for the Host UK monorepo", "name": "perf", "source": "./claude/perf", "description": "Performance profiling helpers for Go and PHP.", diff --git a/claude/core/.claude-plugin/plugin.json b/claude/core/.claude-plugin/plugin.json index e29c4be..e164ac5 100644 --- a/claude/core/.claude-plugin/plugin.json +++ b/claude/core/.claude-plugin/plugin.json @@ -1,5 +1,24 @@ { "name": "core", + "description": "Laravel migration helpers for the Host UK monorepo", + "version": "0.1.0", + "author": { + "name": "Host UK", + "email": "hello@host.uk.com" + }, + "homepage": "https://github.com/host-uk/core-agent", + "repository": { + "type": "git", + "url": "https://github.com/host-uk/core-agent.git" + }, + "license": "EUPL-1.2", + "keywords": [ + "devops", + "monorepo", + "php", + "laravel", + "migrations" + ] "description": "Core functionality - release management", "version": "0.1.0", "author": { diff --git a/claude/core/commands/migrate.md b/claude/core/commands/migrate.md new file mode 100644 index 0000000..d637072 --- /dev/null +++ b/claude/core/commands/migrate.md @@ -0,0 +1,68 @@ +--- +name: migrate +description: Laravel migration helpers +args: [options] +--- + +# Laravel Migration Helper + +Commands to help with Laravel migrations in the monorepo. + +## Usage + +`/core:migrate create [--path ]` - Create a new migration file. +`/core:migrate run` - Run pending migrations. +`/core:migrate rollback` - Rollback the last database migration. +`/core:migrate fresh` - Drop all tables and re-run all migrations. +`/core:migrate status` - Show the status of each migration. +`/core:migrate from-model [--model-path ] [--path ]` - Generate a migration from a model (experimental). + +## Actions + +### Create + +Run this command to create a new migration: + +```bash +"${CLAUDE_PLUGIN_ROOT}/scripts/create.sh" "" "--path" "" +``` + +### Run + +Run this command to run pending migrations: + +```bash +"${CLAUDE_PLUGIN_ROOT}/scripts/run.sh" +``` + +### Rollback + +Run this command to rollback the last migration: + +```bash +"${CLAUDE_PLUGIN_ROOT}/scripts/rollback.sh" +``` + +### Fresh + +Run this command to drop all tables and re-run migrations: + +```bash +"${CLAUDE_PLUGIN_ROOT}/scripts/fresh.sh" +``` + +### Status + +Run this command to check migration status: + +```bash +"${CLAUDE_PLUGIN_ROOT}/scripts/status.sh" +``` + +### From Model + +Run this command to generate a migration from a model: + +```bash +"${CLAUDE_PLUGIN_ROOT}/scripts/from-model.sh" "" "--model-path" "" "--path" "" +``` diff --git a/claude/core/scripts/create.sh b/claude/core/scripts/create.sh new file mode 100644 index 0000000..44570a1 --- /dev/null +++ b/claude/core/scripts/create.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +MIGRATION_NAME="" +MIGRATION_PATH="database/migrations" + +# Parse command-line arguments +while [[ "$#" -gt 0 ]]; do + case $1 in + --path) MIGRATION_PATH="$2"; shift ;; + *) MIGRATION_NAME="$1" ;; + esac + shift +done + +if [ -z "$MIGRATION_NAME" ]; then + echo "Usage: /core:migrate create [--path ]" >&2 + exit 1 +fi + +# Let artisan create the file in the specified path +core php artisan make:migration "$MIGRATION_NAME" --path="$MIGRATION_PATH" > /dev/null + +# Find the newest file in the target directory that matches the name. +FILE_PATH=$(find "$MIGRATION_PATH" -name "*_$MIGRATION_NAME.php" -print -quit) + +if [ -f "$FILE_PATH" ]; then + # Add the workspace_id column and a placeholder for model generation + awk '1; /->id\(\);/ { print " \$table->foreignId(\"workspace_id\")->constrained();\n // --- AUTO-GENERATED COLUMNS GO HERE ---" }' "$FILE_PATH" > "$FILE_PATH.tmp" && mv "$FILE_PATH.tmp" "$FILE_PATH" + # Output just the path for other scripts + echo "$FILE_PATH" +else + echo "ERROR: Could not find created migration file for '$MIGRATION_NAME' in '$MIGRATION_PATH'." >&2 + exit 1 +fi diff --git a/claude/core/scripts/fresh.sh b/claude/core/scripts/fresh.sh new file mode 100644 index 0000000..ebe8bca --- /dev/null +++ b/claude/core/scripts/fresh.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +core php artisan migrate:fresh diff --git a/claude/core/scripts/from-model.sh b/claude/core/scripts/from-model.sh new file mode 100644 index 0000000..ca2abce --- /dev/null +++ b/claude/core/scripts/from-model.sh @@ -0,0 +1,57 @@ +#!/bin/bash +set -e + +MODEL_NAME="" +MODEL_PATH_PREFIX="app/Models" +MIGRATION_PATH="database/migrations" + +# Parse command-line arguments +while [[ "$#" -gt 0 ]]; do + case $1 in + --model-path) MODEL_PATH_PREFIX="$2"; shift ;; + --path) MIGRATION_PATH="$2"; shift ;; + *) MODEL_NAME="$1" ;; + esac + shift +done + +if [ -z "$MODEL_NAME" ]; then + echo "Usage: /core:migrate from-model [--model-path ] [--path ]" + exit 1 +fi + +MODEL_PATH="${MODEL_PATH_PREFIX}/${MODEL_NAME}.php" +TABLE_NAME=$(echo "$MODEL_NAME" | sed 's/\([A-Z]\)/_\L\1/g' | cut -c 2- | sed 's/$/s/') +MIGRATION_NAME="create_${TABLE_NAME}_table" + +if [ ! -f "$MODEL_PATH" ]; then + echo "Model not found at: $MODEL_PATH" + exit 1 +fi + +# Generate the migration file +MIGRATION_FILE=$("${CLAUDE_PLUGIN_ROOT}/scripts/create.sh" "$MIGRATION_NAME" --path "$MIGRATION_PATH") + +if [ ! -f "$MIGRATION_FILE" ]; then + echo "Failed to create migration file." + exit 1 +fi + +# Parse the model using the PHP script +SCHEMA_JSON=$(core php "${CLAUDE_PLUGIN_ROOT}/scripts/parse-model.php" "$MODEL_PATH") + +if echo "$SCHEMA_JSON" | jq -e '.error' > /dev/null; then + echo "Error parsing model: $(echo "$SCHEMA_JSON" | jq -r '.error')" + exit 1 +fi + +# Generate schema definitions from the JSON output +SCHEMA=$(echo "$SCHEMA_JSON" | jq -r '.columns[] | + " $table->" + .type + "(\"" + .name + "\")" + + (if .type == "foreignId" then "->constrained()->onDelete(\"cascade\")" else "" end) + ";" + + (if .index then "\n $table->index(\"" + .name + "\");" else "" end)') + +# Insert the generated schema into the migration file +awk -v schema="$SCHEMA" '{ sub("// --- AUTO-GENERATED COLUMNS GO HERE ---", schema); print }' "$MIGRATION_FILE" > "$MIGRATION_FILE.tmp" && mv "$MIGRATION_FILE.tmp" "$MIGRATION_FILE" + +echo "Generated migration for $MODEL_NAME in $MIGRATION_FILE" diff --git a/claude/core/scripts/parse-model.php b/claude/core/scripts/parse-model.php new file mode 100644 index 0000000..6aae691 --- /dev/null +++ b/claude/core/scripts/parse-model.php @@ -0,0 +1,93 @@ + 'Could not find vendor/autoload.php. Ensure script is run from within a Laravel project.']); + exit(1); +} +require_once $autoload_path; + +if ($argc < 2) { + echo json_encode(['error' => 'Model file path is required.']); + exit(1); +} + +$modelPath = $argv[1]; +if (!file_exists($modelPath)) { + echo json_encode(['error' => "Model file not found at $modelPath"]); + exit(1); +} + +// Convert file path to a class name (e.g., app/Models/User.php -> App\Models\User) +$className = str_replace('.php', '', $modelPath); +$className = ucfirst($className); +$className = str_replace('/', '\\', $className); + + +if (!class_exists($className)) { + echo json_encode(['error' => "Class '$className' could not be found. Check the path and namespace."]); + exit(1); +} + +try { + $reflectionClass = new ReflectionClass($className); + $modelInstance = $reflectionClass->newInstanceWithoutConstructor(); + + // 1. Get columns from the $fillable property + $fillableProperties = $reflectionClass->getDefaultProperties()['fillable'] ?? []; + + $columns = []; + foreach ($fillableProperties as $prop) { + $type = 'string'; // Default type + if (str_ends_with($prop, '_at')) $type = 'timestamp'; + elseif (str_starts_with($prop, 'is_') || str_starts_with($prop, 'has_')) $type = 'boolean'; + elseif (str_ends_with($prop, '_id')) $type = 'foreignId'; + elseif (in_array($prop, ['description', 'content', 'body', 'details', 'notes'])) $type = 'text'; + + $columns[] = ['name' => $prop, 'type' => $type, 'index' => ($type === 'foreignId')]; + } + + // 2. Get foreign keys from BelongsTo relationships + $methods = $reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC); + foreach ($methods as $method) { + if ($method->getNumberOfRequiredParameters() > 0) continue; + + $returnType = $method->getReturnType(); + if ($returnType && $returnType instanceof ReflectionNamedType) { + if (str_ends_with($returnType->getName(), 'BelongsTo')) { + // A BelongsTo relation implies a foreign key column on *this* model's table + $relationName = $method->getName(); + $foreignKey = Illuminate\Support\Str::snake($relationName) . '_id'; + + // Avoid adding duplicates if already found via $fillable + $exists = false; + foreach ($columns as $column) { + if ($column['name'] === $foreignKey) { + $exists = true; + break; + } + } + if (!$exists) { + $columns[] = ['name' => $foreignKey, 'type' => 'foreignId', 'index' => true]; + } + } + } + } + + echo json_encode(['columns' => $columns], JSON_PRETTY_PRINT); + +} catch (ReflectionException $e) { + echo json_encode(['error' => "Reflection error: " . $e->getMessage()]); + exit(1); +} diff --git a/claude/core/scripts/rollback.sh b/claude/core/scripts/rollback.sh new file mode 100644 index 0000000..49b06f1 --- /dev/null +++ b/claude/core/scripts/rollback.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +core php artisan migrate:rollback diff --git a/claude/core/scripts/run.sh b/claude/core/scripts/run.sh new file mode 100644 index 0000000..a0731a5 --- /dev/null +++ b/claude/core/scripts/run.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +core php artisan migrate diff --git a/claude/core/scripts/status.sh b/claude/core/scripts/status.sh new file mode 100644 index 0000000..871404e --- /dev/null +++ b/claude/core/scripts/status.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +core php artisan migrate:status