|
4 | 4 | import { Button } from "$lib/components/ui/button";
|
5 | 5 | import { currentRequest } from "$lib/store";
|
6 | 6 | import { convertDateToHumanReadable } from "$lib/utils";
|
7 |
| - import { ArrowLeft, ArrowUpRight, Clock, Loader, Play, RefreshCw } from "lucide-svelte"; |
| 7 | + import { ArrowLeft, ArrowUpRight, Clock, Copy, Loader, Play, RefreshCw } from "lucide-svelte"; |
8 | 8 | import Highlight from "svelte-highlight";
|
9 | 9 | import json from "svelte-highlight/languages/json";
|
10 | 10 | import atomonelight from "svelte-highlight/styles/atom-one-light";
|
|
42 | 42 | replaying = false;
|
43 | 43 | }
|
44 | 44 | };
|
| 45 | +
|
| 46 | + const generateCurlCommand = () => { |
| 47 | + if (!$currentRequest) return ''; |
| 48 | +
|
| 49 | + // Construct full tunnel URL |
| 50 | + const tunnelUrl = `https://${$currentRequest.Host}${$currentRequest.Url}`; |
| 51 | + let curl = `curl -X ${$currentRequest.Method} '${tunnelUrl}'`; |
| 52 | +
|
| 53 | + // Add headers |
| 54 | + const contentType = $currentRequest.Headers?.['Content-Type']?.[0] || ''; |
| 55 | + const isMultipartForm = contentType.startsWith('multipart/form-data'); |
| 56 | +
|
| 57 | + if ($currentRequest.Headers) { |
| 58 | + Object.entries($currentRequest.Headers).forEach(([key, value]) => { |
| 59 | + if (Array.isArray(value) && value.length > 0 && key !== 'Content-Type' && key !== 'Content-Length') { |
| 60 | + curl += ` \\\n -H '${key}: ${value[0]}'`; |
| 61 | + } |
| 62 | + }); |
| 63 | + } |
| 64 | +
|
| 65 | + // Add body if present |
| 66 | + if ($currentRequest.Body) { |
| 67 | + try { |
| 68 | + // First decode from base64 |
| 69 | + const decodedBytes = atob($currentRequest.Body); |
| 70 | +
|
| 71 | + if (isMultipartForm) { |
| 72 | + // For multipart form data, we'll use -F instead of -d |
| 73 | + // Extract boundary from content type |
| 74 | + const boundaryMatch = contentType.match(/boundary=([^;]+)/); |
| 75 | + if (boundaryMatch) { |
| 76 | + const boundary = boundaryMatch[1]; |
| 77 | + const parts = decodedBytes.split('--' + boundary); |
| 78 | +
|
| 79 | + // Process each part |
| 80 | + parts.forEach(part => { |
| 81 | + if (part.trim() && !part.includes('--\r\n')) { |
| 82 | + const contentDispositionMatch = part.match(/Content-Disposition: form-data; name="([^"]+)"(?:; filename="([^"]+)")?/); |
| 83 | + if (contentDispositionMatch) { |
| 84 | + const name = contentDispositionMatch[1]; |
| 85 | + const filename = contentDispositionMatch[2]; |
| 86 | +
|
| 87 | + if (filename) { |
| 88 | + // For file uploads, use a placeholder |
| 89 | + curl += ` \\\n -F '${name}=@path/to/${filename}'`; |
| 90 | + } else { |
| 91 | + // For regular form fields, extract the value |
| 92 | + const value = part.split('\r\n\r\n')[1]?.trim(); |
| 93 | + if (value) { |
| 94 | + curl += ` \\\n -F '${name}=${value}'`; |
| 95 | + } |
| 96 | + } |
| 97 | + } |
| 98 | + } |
| 99 | + }); |
| 100 | + } |
| 101 | + } else { |
| 102 | + // For non-multipart data, use -d as before |
| 103 | + try { |
| 104 | + const decodedBody = decodeURIComponent(decodedBytes); |
| 105 | + curl += ` \\\n -d '${decodedBody}'`; |
| 106 | + } catch { |
| 107 | + curl += ` \\\n -d '${decodedBytes}'`; |
| 108 | + } |
| 109 | + } |
| 110 | + } catch (e) { |
| 111 | + curl += ` \\\n -d '${$currentRequest.Body}'`; |
| 112 | + } |
| 113 | + } |
| 114 | +
|
| 115 | + return curl; |
| 116 | + }; |
| 117 | +
|
| 118 | + const copyCurlCommand = async () => { |
| 119 | + const curl = generateCurlCommand(); |
| 120 | + try { |
| 121 | + await navigator.clipboard.writeText(curl); |
| 122 | + toast.success('Curl command copied to clipboard'); |
| 123 | + } catch (error) { |
| 124 | + toast.error('Failed to copy curl command'); |
| 125 | + } |
| 126 | + }; |
45 | 127 | </script>
|
46 | 128 |
|
47 | 129 | <svelte:head>
|
|
107 | 189 | Replay
|
108 | 190 | {/if}
|
109 | 191 | </Button>
|
| 192 | + <Button |
| 193 | + variant="outline" |
| 194 | + size="sm" |
| 195 | + on:click={copyCurlCommand} |
| 196 | + class="flex items-center gap-2" |
| 197 | + > |
| 198 | + <Copy class="w-4 h-4" /> |
| 199 | + Copy as cURL |
| 200 | + </Button> |
110 | 201 | </div>
|
111 | 202 | </div>
|
112 | 203 | </div>
|
|
0 commit comments