diff --git a/claude/code/commands/api.md b/claude/code/commands/api.md new file mode 100644 index 0000000..fdffe41 --- /dev/null +++ b/claude/code/commands/api.md @@ -0,0 +1,27 @@ +--- +name: api +description: Generate TypeScript/JavaScript API client from Laravel routes +args: generate [--ts|--js|--openapi] +--- + +# API Client Generator + +Generate a TypeScript/JavaScript API client or an OpenAPI specification from your Laravel routes. + +## Usage + +Generate a TypeScript client (default): +`/code:api generate` +`/code:api generate --ts` + +Generate a JavaScript client: +`/code:api generate --js` + +Generate an OpenAPI specification: +`/code:api generate --openapi` + +## Action + +```bash +"${CLAUDE_PLUGIN_ROOT}/scripts/api-generate.sh" "$@" +``` diff --git a/claude/code/scripts/api-generate.sh b/claude/code/scripts/api-generate.sh new file mode 100755 index 0000000..93628e0 --- /dev/null +++ b/claude/code/scripts/api-generate.sh @@ -0,0 +1,211 @@ +#!/bin/bash + +# Default values +output_format="ts" +routes_file="routes/api.php" +output_file="api_client" # Default output file name without extension + +# Parse command-line arguments +while [[ "$#" -gt 0 ]]; do + case $1 in + generate) ;; # Skip the generate subcommand + --ts) output_format="ts";; + --js) output_format="js";; + --openapi) output_format="openapi";; + *) routes_file="$1";; + esac + shift +done + +# Set the output file extension based on format +if [[ "$output_format" == "openapi" ]]; then + output_file="openapi.json" +else + output_file="api_client.${output_format}" +fi + +# Function to parse the routes file +parse_routes() { + if [ ! -f "$1" ]; then + echo "Error: Routes file not found at $1" >&2 + exit 1 + fi + awk -F"'" ' + /Route::apiResource/ { + resource = $2; + resource_singular = resource; + sub(/s$/, "", resource_singular); + print "GET " resource " list"; + print "POST " resource " create"; + print "GET " resource "/{" resource_singular "} get"; + print "PUT " resource "/{" resource_singular "} update"; + print "DELETE " resource "/{" resource_singular "} delete"; + } + /Route::(get|post|put|delete|patch)/ { + line = $0; + match(line, /Route::([a-z]+)/, m); + method = toupper(m[1]); + uri = $2; + action = $6; + print method " " uri " " action; + } + ' "$1" +} + +# Function to generate the API client +generate_client() { + local format=$1 + local outfile=$2 + local client_object="export const api = {\n" + local dto_definitions="" + declare -A dtos + + declare -A groups + + # First pass: Collect all routes and DTOs + while read -r method uri action; do + group=$(echo "$uri" | cut -d'/' -f1) + if [[ -z "${groups[$group]}" ]]; then + groups[$group]="" + fi + groups[$group]+="$method $uri $action\n" + + if [[ "$method" == "POST" || "$method" == "PUT" || "$method" == "PATCH" ]]; then + local resource_name_for_dto=$(echo "$group" | sed 's/s$//' | awk '{print toupper(substr($0,0,1))substr($0,2)}') + local dto_name="$(tr '[:lower:]' '[:upper:]' <<< ${action:0:1})${action:1}${resource_name_for_dto}Dto" + dtos[$dto_name]=1 + fi + done + + # Generate DTO interface definitions for TypeScript + if [ "$format" == "ts" ]; then + for dto in $(echo "${!dtos[@]}" | tr ' ' '\n' | sort); do + dto_definitions+="export interface ${dto} {}\n" + done + dto_definitions+="\n" + fi + + # Sort the group names alphabetically to ensure consistent output + sorted_groups=$(for group in "${!groups[@]}"; do echo "$group"; done | sort) + + for group in $sorted_groups; do + client_object+=" ${group}: {\n" + + # Sort the lines within the group by the action name (field 3) + sorted_lines=$(echo -e "${groups[$group]}" | sed '/^$/d' | sort -k3) + + while IFS= read -r line; do + if [ -z "$line" ]; then continue; fi + method=$(echo "$line" | cut -d' ' -f1) + uri=$(echo "$line" | cut -d' ' -f2) + action=$(echo "$line" | cut -d' ' -f3) + + params=$(echo "$uri" | grep -o '{[^}]*}' | sed 's/[{}]//g') + ts_types="" + js_args="" + + # Generate arguments for the function signature + for p in $params; do + js_args+="${p}, " + ts_types+="${p}: number, " + done + + # Add a 'data' argument for POST/PUT/PATCH methods + if [[ "$method" == "POST" || "$method" == "PUT" || "$method" == "PATCH" ]]; then + local resource_name_for_dto=$(echo "$group" | sed 's/s$//' | awk '{print toupper(substr($0,0,1))substr($0,2)}') + local dto_name="$(tr '[:lower:]' '[:upper:]' <<< ${action:0:1})${action:1}${resource_name_for_dto}Dto" + ts_types+="data: ${dto_name}" + js_args+="data" + fi + + # Clean up function arguments string + func_args=$(echo "$ts_types" | sed 's/,\s*$//' | sed 's/,$//') + js_args=$(echo "$js_args" | sed 's/,\s*$//' | sed 's/,$//') + + final_args=$([ "$format" == "ts" ] && echo "$func_args" || echo "$js_args") + + # Construct the fetch call string + fetch_uri="/api/${uri}" + fetch_uri=$(echo "$fetch_uri" | sed 's/{/${/g') + + client_object+=" ${action}: (${final_args}) => fetch(\`${fetch_uri}\`" + + # Add request options for non-GET methods + if [ "$method" != "GET" ]; then + client_object+=", {\n method: '${method}'" + if [[ "$method" == "POST" || "$method" == "PUT" || "$method" == "PATCH" ]]; then + client_object+=", \n body: JSON.stringify(data)" + fi + client_object+="\n }" + fi + client_object+="),\n" + + done <<< "$sorted_lines" + client_object+=" },\n" + done + + client_object+="};" + + echo -e "// Generated from ${routes_file}\n" > "$outfile" + echo -e "${dto_definitions}${client_object}" >> "$outfile" + echo "API client generated at ${outfile}" +} + +# Function to generate OpenAPI spec +generate_openapi() { + local outfile=$1 + local paths_json="" + + declare -A paths + while read -r method uri action; do + path="/api/${uri}" + # OpenAPI uses lowercase methods + method_lower=$(echo "$method" | tr '[:upper:]' '[:lower:]') + + # Group operations by path + if [[ -z "${paths[$path]}" ]]; then + paths[$path]="" + fi + paths[$path]+="\"${method_lower}\": {\"summary\": \"${action}\"}," + done + + # Assemble the paths object + sorted_paths=$(for path in "${!paths[@]}"; do echo "$path"; done | sort) + for path in $sorted_paths; do + operations=$(echo "${paths[$path]}" | sed 's/,$//') # remove trailing comma + paths_json+="\"${path}\": {${operations}}," + done + paths_json=$(echo "$paths_json" | sed 's/,$//') # remove final trailing comma + + # Create the final OpenAPI JSON structure + openapi_spec=$(cat < "$outfile" + echo "OpenAPI spec generated at ${outfile}" +} + + +# Main logic +parsed_routes=$(parse_routes "$routes_file") + +if [[ "$output_format" == "ts" || "$output_format" == "js" ]]; then + generate_client "$output_format" "$output_file" <<< "$parsed_routes" +elif [[ "$output_format" == "openapi" ]]; then + generate_openapi "$output_file" <<< "$parsed_routes" +else + echo "Invalid output format specified." >&2 + exit 1 +fi