From 4b76b3422c4e381578dfc738c2723f35ad9cdcb3 Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Wed, 11 Feb 2026 05:56:48 -0800 Subject: [PATCH] refactor: reduce complexity in execScript and netcup pick functions (#449) Extract error handling from execScript() into dedicated helpers (reportDownloadError, reportScriptFailure, getScriptFailureGuidance), reducing the function from 52 to 15 lines and making error guidance directly testable. Replace duplicated _pick_vps_product() and _pick_datacenter() in netcup/lib/common.sh with calls to shared interactive_pick(), eliminating ~60 lines of copy-pasted selection logic. Net reduction: 42 lines (-98/+56). Agent: complexity-hunter Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) --- cli/src/commands.ts | 85 +++++++++++++++++++++++++++----------------- netcup/lib/common.sh | 69 +++-------------------------------- 2 files changed, 56 insertions(+), 98 deletions(-) diff --git a/cli/src/commands.ts b/cli/src/commands.ts index 269ba257..d60bc271 100644 --- a/cli/src/commands.ts +++ b/cli/src/commands.ts @@ -407,6 +407,56 @@ function reportDownloadFailure(primaryUrl: string, fallbackUrl: string, primaryS } } +function reportDownloadError(ghUrl: string, err: unknown): never { + p.log.error("Failed to download spawn script"); + console.error("\nError:", getErrorMessage(err)); + console.error("\nTroubleshooting:"); + console.error(` 1. Verify this combination exists: ${pc.cyan("spawn list")}`); + console.error(" 2. Check your internet connection"); + console.error(` 3. Try accessing the script directly: ${ghUrl}`); + process.exit(1); +} + +export function getScriptFailureGuidance(exitCode: number | null, cloud: string): string[] { + switch (exitCode) { + case 127: + return [ + "A required command was not found. Check that these are installed:", + " - bash, curl, ssh, jq", + ` - Cloud-specific CLI tools (run ${pc.cyan(`spawn ${cloud}`)} for details)`, + ]; + case 126: + return ["A command was found but could not be executed (permission denied)."]; + case 1: + return [ + "Common causes:", + ` - Missing or invalid credentials (run ${pc.cyan(`spawn ${cloud}`)} for setup)`, + " - Cloud provider API error (quota, rate limit, or region issue)", + " - Server provisioning failed (try again or pick a different region)", + ]; + default: + return [ + "Common causes:", + ` - Missing credentials (run ${pc.cyan(`spawn ${cloud}`)} for setup instructions)`, + " - Cloud provider API rate limit or quota exceeded", + " - Missing local dependencies (SSH, curl, jq)", + ]; + } +} + +function reportScriptFailure(errMsg: string, cloud: string): never { + p.log.error("Spawn script failed"); + console.error("\nError:", errMsg); + + const exitCodeMatch = errMsg.match(/exited with code (\d+)/); + const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : null; + + const lines = getScriptFailureGuidance(exitCode, cloud); + console.error(""); + for (const line of lines) console.error(line); + process.exit(1); +} + async function execScript(cloud: string, agent: string, prompt?: string): Promise { const url = `https://openrouter.ai/lab/spawn/${cloud}/${agent}.sh`; const ghUrl = `${RAW_BASE}/${cloud}/${agent}.sh`; @@ -415,13 +465,7 @@ async function execScript(cloud: string, agent: string, prompt?: string): Promis try { scriptContent = await downloadScriptWithFallback(url, ghUrl); } catch (err) { - p.log.error("Failed to download spawn script"); - console.error("\nError:", getErrorMessage(err)); - console.error("\nTroubleshooting:"); - console.error(` 1. Verify this combination exists: ${pc.cyan("spawn list")}`); - console.error(" 2. Check your internet connection"); - console.error(` 3. Try accessing the script directly: ${ghUrl}`); - process.exit(1); + reportDownloadError(ghUrl, err); } try { @@ -429,34 +473,9 @@ async function execScript(cloud: string, agent: string, prompt?: string): Promis } catch (err) { const errMsg = getErrorMessage(err); if (errMsg.includes("interrupted by user")) { - // User pressed Ctrl+C - exit silently process.exit(130); } - p.log.error("Spawn script failed"); - console.error("\nError:", errMsg); - - // Extract exit code from error message for targeted guidance - const exitCodeMatch = errMsg.match(/exited with code (\d+)/); - const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : null; - - if (exitCode === 127) { - console.error("\nA required command was not found. Check that these are installed:"); - console.error(" - bash, curl, ssh, jq"); - console.error(` - Cloud-specific CLI tools (run ${pc.cyan(`spawn ${cloud}`)} for details)`); - } else if (exitCode === 126) { - console.error("\nA command was found but could not be executed (permission denied)."); - } else if (exitCode === 1) { - console.error("\nCommon causes:"); - console.error(` - Missing or invalid credentials (run ${pc.cyan(`spawn ${cloud}`)} for setup)`); - console.error(" - Cloud provider API error (quota, rate limit, or region issue)"); - console.error(" - Server provisioning failed (try again or pick a different region)"); - } else { - console.error("\nCommon causes:"); - console.error(` - Missing credentials (run ${pc.cyan(`spawn ${cloud}`)} for setup instructions)`); - console.error(" - Cloud provider API rate limit or quota exceeded"); - console.error(" - Missing local dependencies (SSH, curl, jq)"); - } - process.exit(1); + reportScriptFailure(errMsg, cloud); } } diff --git a/netcup/lib/common.sh b/netcup/lib/common.sh index 6ab61b12..c84e0b78 100644 --- a/netcup/lib/common.sh +++ b/netcup/lib/common.sh @@ -223,43 +223,9 @@ for p in sorted(products, key=lambda x: float(x.get('price', 999))): " } -# Interactive VPS product picker +# Interactive VPS product picker (delegates to shared interactive_pick) _pick_vps_product() { - if [[ -n "${NETCUP_VPS_PRODUCT:-}" ]]; then - echo "$NETCUP_VPS_PRODUCT" - return - fi - - log_info "Fetching available VPS products..." - local products - products=$(_list_vps_products) - - if [[ -z "$products" ]]; then - log_warn "Could not fetch VPS products, using default: VPS 200 G10" - echo "VPS 200 G10" - return - fi - - log_info "Available VPS products:" - local i=1 - local names=() - while IFS='|' read -r name cores ram disk price; do - printf " %2d) %-15s %-8s %-12s %-13s %s\n" "$i" "$name" "$cores" "$ram" "$disk" "$price" >&2 - names+=("$name") - i=$((i + 1)) - done <<< "$products" - - local choice - printf "\n" >&2 - choice=$(safe_read "Select VPS product [1]: ") || choice="" - choice="${choice:-1}" - - if [[ "$choice" -ge 1 && "$choice" -le "${#names[@]}" ]] 2>/dev/null; then - echo "${names[$((choice - 1))]}" - else - log_warn "Invalid choice, using default: VPS 200 G10" - echo "VPS 200 G10" - fi + interactive_pick "NETCUP_VPS_PRODUCT" "VPS 200 G10" "VPS products" "_list_vps_products" } # List available datacenters @@ -269,36 +235,9 @@ _list_datacenters() { echo "Vienna|AT|Austria" } -# Interactive datacenter picker +# Interactive datacenter picker (delegates to shared interactive_pick) _pick_datacenter() { - if [[ -n "${NETCUP_DATACENTER:-}" ]]; then - echo "$NETCUP_DATACENTER" - return - fi - - log_info "Available datacenters:" - local datacenters - datacenters=$(_list_datacenters) - - local i=1 - local names=() - while IFS='|' read -r name country_code country; do - printf " %2d) %-12s %s (%s)\n" "$i" "$name" "$country" "$country_code" >&2 - names+=("$name") - i=$((i + 1)) - done <<< "$datacenters" - - local choice - printf "\n" >&2 - choice=$(safe_read "Select datacenter [1]: ") || choice="" - choice="${choice:-1}" - - if [[ "$choice" -ge 1 && "$choice" -le "${#names[@]}" ]] 2>/dev/null; then - echo "${names[$((choice - 1))]}" - else - log_warn "Invalid choice, using default: Nuremberg" - echo "Nuremberg" - fi + interactive_pick "NETCUP_DATACENTER" "Nuremberg" "datacenters" "_list_datacenters" } # Build JSON request body for Netcup VPS creation