@theihtisham/devtools-with-cloud 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/.env.example +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +73 -0
  4. package/docker-compose.yml +23 -0
  5. package/jest.config.js +7 -0
  6. package/next-env.d.ts +5 -0
  7. package/next.config.mjs +22 -0
  8. package/package.json +82 -0
  9. package/postcss.config.js +6 -0
  10. package/prisma/schema.prisma +105 -0
  11. package/prisma/seed.ts +211 -0
  12. package/src/app/(app)/ai/page.tsx +122 -0
  13. package/src/app/(app)/collections/page.tsx +155 -0
  14. package/src/app/(app)/environments/page.tsx +96 -0
  15. package/src/app/(app)/history/page.tsx +107 -0
  16. package/src/app/(app)/import/page.tsx +102 -0
  17. package/src/app/(app)/layout.tsx +60 -0
  18. package/src/app/(app)/settings/page.tsx +79 -0
  19. package/src/app/(app)/workspace/page.tsx +284 -0
  20. package/src/app/api/ai/discover/route.ts +17 -0
  21. package/src/app/api/ai/explain/route.ts +29 -0
  22. package/src/app/api/ai/generate-tests/route.ts +37 -0
  23. package/src/app/api/ai/suggest/route.ts +29 -0
  24. package/src/app/api/collections/[id]/route.ts +66 -0
  25. package/src/app/api/collections/route.ts +48 -0
  26. package/src/app/api/environments/route.ts +40 -0
  27. package/src/app/api/export/openapi/route.ts +17 -0
  28. package/src/app/api/export/postman/route.ts +18 -0
  29. package/src/app/api/import/curl/route.ts +18 -0
  30. package/src/app/api/import/har/route.ts +20 -0
  31. package/src/app/api/import/openapi/route.ts +21 -0
  32. package/src/app/api/import/postman/route.ts +21 -0
  33. package/src/app/api/proxy/route.ts +35 -0
  34. package/src/app/api/requests/[id]/execute/route.ts +85 -0
  35. package/src/app/api/requests/[id]/history/route.ts +23 -0
  36. package/src/app/api/requests/[id]/route.ts +66 -0
  37. package/src/app/api/requests/route.ts +49 -0
  38. package/src/app/api/workspaces/route.ts +38 -0
  39. package/src/app/globals.css +99 -0
  40. package/src/app/layout.tsx +24 -0
  41. package/src/app/page.tsx +182 -0
  42. package/src/components/ai/ai-panel.tsx +65 -0
  43. package/src/components/ai/code-explainer.tsx +51 -0
  44. package/src/components/ai/endpoint-discovery.tsx +62 -0
  45. package/src/components/ai/test-generator.tsx +49 -0
  46. package/src/components/collections/collection-actions.tsx +36 -0
  47. package/src/components/collections/collection-tree.tsx +55 -0
  48. package/src/components/collections/folder-creator.tsx +54 -0
  49. package/src/components/landing/comparison.tsx +43 -0
  50. package/src/components/landing/cta.tsx +16 -0
  51. package/src/components/landing/features.tsx +24 -0
  52. package/src/components/landing/hero.tsx +23 -0
  53. package/src/components/response/body-viewer.tsx +33 -0
  54. package/src/components/response/headers-viewer.tsx +23 -0
  55. package/src/components/response/status-badge.tsx +25 -0
  56. package/src/components/response/test-results.tsx +50 -0
  57. package/src/components/response/timing-chart.tsx +39 -0
  58. package/src/components/ui/badge.tsx +24 -0
  59. package/src/components/ui/button.tsx +32 -0
  60. package/src/components/ui/code-editor.tsx +51 -0
  61. package/src/components/ui/dialog.tsx +56 -0
  62. package/src/components/ui/dropdown.tsx +63 -0
  63. package/src/components/ui/input.tsx +22 -0
  64. package/src/components/ui/key-value-editor.tsx +75 -0
  65. package/src/components/ui/select.tsx +24 -0
  66. package/src/components/ui/tabs.tsx +85 -0
  67. package/src/components/ui/textarea.tsx +22 -0
  68. package/src/components/ui/toast.tsx +54 -0
  69. package/src/components/workspace/request-panel.tsx +38 -0
  70. package/src/components/workspace/response-panel.tsx +81 -0
  71. package/src/components/workspace/sidebar.tsx +52 -0
  72. package/src/components/workspace/split-pane.tsx +49 -0
  73. package/src/components/workspace/tabs/auth-tab.tsx +94 -0
  74. package/src/components/workspace/tabs/body-tab.tsx +41 -0
  75. package/src/components/workspace/tabs/headers-tab.tsx +23 -0
  76. package/src/components/workspace/tabs/params-tab.tsx +23 -0
  77. package/src/components/workspace/tabs/pre-request-tab.tsx +26 -0
  78. package/src/components/workspace/url-bar.tsx +53 -0
  79. package/src/hooks/use-ai.ts +115 -0
  80. package/src/hooks/use-collection.ts +71 -0
  81. package/src/hooks/use-environment.ts +73 -0
  82. package/src/hooks/use-request.ts +111 -0
  83. package/src/lib/ai/endpoint-discovery.ts +158 -0
  84. package/src/lib/ai/explainer.ts +127 -0
  85. package/src/lib/ai/suggester.ts +164 -0
  86. package/src/lib/ai/test-generator.ts +161 -0
  87. package/src/lib/auth/api-key.ts +28 -0
  88. package/src/lib/auth/aws-sig.ts +131 -0
  89. package/src/lib/auth/basic.ts +17 -0
  90. package/src/lib/auth/bearer.ts +15 -0
  91. package/src/lib/auth/oauth2.ts +155 -0
  92. package/src/lib/auth/types.ts +16 -0
  93. package/src/lib/db/client.ts +15 -0
  94. package/src/lib/env/manager.ts +32 -0
  95. package/src/lib/env/resolver.ts +30 -0
  96. package/src/lib/exporters/openapi.ts +193 -0
  97. package/src/lib/exporters/postman.ts +140 -0
  98. package/src/lib/graphql/builder.ts +249 -0
  99. package/src/lib/graphql/formatter.ts +147 -0
  100. package/src/lib/graphql/index.ts +43 -0
  101. package/src/lib/graphql/introspection.ts +175 -0
  102. package/src/lib/graphql/types.ts +99 -0
  103. package/src/lib/graphql/validator.ts +216 -0
  104. package/src/lib/http/client.ts +112 -0
  105. package/src/lib/http/proxy.ts +83 -0
  106. package/src/lib/http/request-builder.ts +214 -0
  107. package/src/lib/http/response-parser.ts +106 -0
  108. package/src/lib/http/timing.ts +63 -0
  109. package/src/lib/importers/curl-parser.ts +346 -0
  110. package/src/lib/importers/har-parser.ts +128 -0
  111. package/src/lib/importers/openapi.ts +324 -0
  112. package/src/lib/importers/postman.ts +312 -0
  113. package/src/lib/test-runner/assertions.ts +163 -0
  114. package/src/lib/test-runner/reporter.ts +90 -0
  115. package/src/lib/test-runner/runner.ts +69 -0
  116. package/src/lib/utils/api-response.ts +85 -0
  117. package/src/lib/utils/cn.ts +6 -0
  118. package/src/lib/utils/content-type.ts +123 -0
  119. package/src/lib/utils/download.ts +53 -0
  120. package/src/lib/utils/errors.ts +92 -0
  121. package/src/lib/utils/format.ts +142 -0
  122. package/src/lib/utils/syntax-highlight.ts +108 -0
  123. package/src/lib/utils/validation.ts +231 -0
  124. package/src/lib/websocket/client.ts +182 -0
  125. package/src/lib/websocket/frames.ts +96 -0
  126. package/src/lib/websocket/history.ts +121 -0
  127. package/src/lib/websocket/index.ts +25 -0
  128. package/src/lib/websocket/types.ts +57 -0
  129. package/src/types/ai.ts +28 -0
  130. package/src/types/collection.ts +24 -0
  131. package/src/types/environment.ts +16 -0
  132. package/src/types/request.ts +54 -0
  133. package/src/types/response.ts +37 -0
  134. package/tailwind.config.ts +82 -0
  135. package/tests/lib/env/resolver.test.ts +108 -0
  136. package/tests/lib/graphql/builder.test.ts +349 -0
  137. package/tests/lib/graphql/formatter.test.ts +99 -0
  138. package/tests/lib/http/request-builder.test.ts +160 -0
  139. package/tests/lib/http/response-parser.test.ts +150 -0
  140. package/tests/lib/http/timing.test.ts +188 -0
  141. package/tests/lib/importers/curl-parser.test.ts +245 -0
  142. package/tests/lib/test-runner/assertions.test.ts +342 -0
  143. package/tests/lib/utils/cn.test.ts +46 -0
  144. package/tests/lib/utils/content-type.test.ts +175 -0
  145. package/tests/lib/utils/format.test.ts +188 -0
  146. package/tests/lib/utils/validation.test.ts +237 -0
  147. package/tests/lib/websocket/history.test.ts +186 -0
  148. package/tsconfig.json +29 -0
  149. package/tsconfig.tsbuildinfo +1 -0
  150. package/vitest.config.ts +21 -0
@@ -0,0 +1,346 @@
1
+ import type { HttpMethod } from '@/lib/utils/validation';
2
+
3
+ export interface ParsedCurl {
4
+ method: HttpMethod;
5
+ url: string;
6
+ headers: { key: string; value: string; enabled: boolean }[];
7
+ params: { key: string; value: string; enabled: boolean }[];
8
+ body?: string;
9
+ bodyType?: string;
10
+ auth?: unknown;
11
+ name?: string;
12
+ }
13
+
14
+ /**
15
+ * Parse a cURL command string into a structured request object.
16
+ * Handles common cURL flags: -X, -H, -d, --data, --data-raw, -F, -u, -b, etc.
17
+ */
18
+ export function parseCurl(command: string): ParsedCurl {
19
+ const tokens = tokenizeCurl(command);
20
+
21
+ if (tokens.length === 0) {
22
+ throw new Error('Empty cURL command');
23
+ }
24
+
25
+ // Remove 'curl' from the start if present
26
+ if (tokens[0]?.toLowerCase() === 'curl') {
27
+ tokens.shift();
28
+ }
29
+
30
+ let method: HttpMethod = 'GET';
31
+ let url = '';
32
+ const headers: ParsedCurl['headers'] = [];
33
+ const params: ParsedCurl['params'] = [];
34
+ let body: string | undefined;
35
+ let bodyType: string | undefined;
36
+ let auth: unknown;
37
+ let hasData = false;
38
+
39
+ let i = 0;
40
+ while (i < tokens.length) {
41
+ const token = tokens[i];
42
+
43
+ if (token === undefined) { i++; continue; }
44
+
45
+ switch (token) {
46
+ // Method
47
+ case '-X':
48
+ case '--request': {
49
+ const val = tokens[++i];
50
+ if (val) method = val.toUpperCase() as HttpMethod;
51
+ break;
52
+ }
53
+
54
+ // Headers
55
+ case '-H':
56
+ case '--header': {
57
+ const headerStr = tokens[++i];
58
+ if (headerStr) {
59
+ const colonIndex = headerStr.indexOf(':');
60
+ if (colonIndex > 0) {
61
+ const key = headerStr.slice(0, colonIndex).trim();
62
+ const value = headerStr.slice(colonIndex + 1).trim();
63
+ headers.push({ key, value, enabled: true });
64
+ }
65
+ }
66
+ break;
67
+ }
68
+
69
+ // Data
70
+ case '-d':
71
+ case '--data':
72
+ case '--data-raw':
73
+ case '--data-binary': {
74
+ const dataVal = tokens[++i];
75
+ if (dataVal) {
76
+ body = dataVal;
77
+ hasData = true;
78
+
79
+ // Determine body type
80
+ if (dataVal.trim().startsWith('{') || dataVal.trim().startsWith('[')) {
81
+ bodyType = 'json';
82
+ } else if (dataVal.includes('=') && !dataVal.startsWith('@')) {
83
+ bodyType = 'urlencoded';
84
+ } else {
85
+ bodyType = 'raw';
86
+ }
87
+
88
+ // Default to POST when data is present
89
+ if (method === 'GET') method = 'POST';
90
+ }
91
+ break;
92
+ }
93
+
94
+ // Form data
95
+ case '-F':
96
+ case '--form': {
97
+ const formVal = tokens[++i];
98
+ if (formVal) {
99
+ bodyType = 'form';
100
+ if (!body) body = formVal;
101
+ else body += `&${formVal}`;
102
+
103
+ if (method === 'GET') method = 'POST';
104
+ hasData = true;
105
+ }
106
+ break;
107
+ }
108
+
109
+ // User (Basic Auth)
110
+ case '-u':
111
+ case '--user': {
112
+ const userVal = tokens[++i];
113
+ if (userVal) {
114
+ const colonIndex = userVal.indexOf(':');
115
+ if (colonIndex > 0) {
116
+ auth = {
117
+ type: 'basic',
118
+ username: userVal.slice(0, colonIndex),
119
+ password: userVal.slice(colonIndex + 1),
120
+ };
121
+ } else {
122
+ auth = { type: 'basic', username: userVal, password: '' };
123
+ }
124
+ }
125
+ break;
126
+ }
127
+
128
+ // Bearer token
129
+ case '--oauth2-bearer': {
130
+ const tokenVal = tokens[++i];
131
+ if (tokenVal) {
132
+ auth = { type: 'bearer', token: tokenVal, prefix: 'Bearer' };
133
+ }
134
+ break;
135
+ }
136
+
137
+ // Cookies
138
+ case '-b':
139
+ case '--cookie': {
140
+ const cookieVal = tokens[++i];
141
+ if (cookieVal) {
142
+ headers.push({ key: 'Cookie', value: cookieVal, enabled: true });
143
+ }
144
+ break;
145
+ }
146
+
147
+ // Follow redirects
148
+ case '-L':
149
+ case '--location':
150
+ // Note: we'd set followRedirects = true on the request config
151
+ break;
152
+
153
+ // Insecure SSL
154
+ case '-k':
155
+ case '--insecure':
156
+ // Note: we'd set verifySSL = false on the request config
157
+ break;
158
+
159
+ // Silent/verbose (ignore)
160
+ case '-s':
161
+ case '--silent':
162
+ case '-v':
163
+ case '--verbose':
164
+ case '-S':
165
+ case '--show-error':
166
+ break;
167
+
168
+ // Output file (ignore)
169
+ case '-o':
170
+ case '--output':
171
+ i++; // Skip the filename
172
+ break;
173
+
174
+ // Max time
175
+ case '-m':
176
+ case '--max-time':
177
+ i++; // Skip the time value
178
+ break;
179
+
180
+ // Connection timeout
181
+ case '--connect-timeout':
182
+ i++; // Skip the time value
183
+ break;
184
+
185
+ // User agent
186
+ case '-A':
187
+ case '--user-agent': {
188
+ const uaVal = tokens[++i];
189
+ if (uaVal) {
190
+ headers.push({ key: 'User-Agent', value: uaVal, enabled: true });
191
+ }
192
+ break;
193
+ }
194
+
195
+ // No default method override
196
+ case '--compressed':
197
+ headers.push({ key: 'Accept-Encoding', value: 'gzip, deflate', enabled: true });
198
+ break;
199
+
200
+ default:
201
+ // URL (first non-flag argument)
202
+ if (!token.startsWith('-') && !url) {
203
+ url = token;
204
+
205
+ // Remove quotes if present
206
+ if ((url.startsWith("'") && url.endsWith("'")) || (url.startsWith('"') && url.endsWith('"'))) {
207
+ url = url.slice(1, -1);
208
+ }
209
+ }
210
+ break;
211
+ }
212
+
213
+ i++;
214
+ }
215
+
216
+ if (!url) {
217
+ throw new Error('No URL found in cURL command');
218
+ }
219
+
220
+ // Parse query params from URL
221
+ try {
222
+ const urlObj = new URL(url);
223
+ urlObj.searchParams.forEach((value, key) => {
224
+ params.push({ key, value, enabled: true });
225
+ });
226
+ // Use URL without query string for cleaner display
227
+ url = urlObj.origin + urlObj.pathname;
228
+ } catch {
229
+ // URL might have variables, keep as-is
230
+ }
231
+
232
+ return {
233
+ method,
234
+ url,
235
+ headers,
236
+ params,
237
+ body,
238
+ bodyType,
239
+ auth,
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Tokenize a cURL command string respecting quotes and escaped characters.
245
+ */
246
+ function tokenizeCurl(command: string): string[] {
247
+ const tokens: string[] = [];
248
+ let current = '';
249
+ let inSingleQuote = false;
250
+ let inDoubleQuote = false;
251
+ let escaped = false;
252
+
253
+ for (let i = 0; i < command.length; i++) {
254
+ const char = command[i];
255
+
256
+ if (char === undefined) continue;
257
+
258
+ if (escaped) {
259
+ current += char;
260
+ escaped = false;
261
+ continue;
262
+ }
263
+
264
+ if (char === '\\') {
265
+ if (inSingleQuote) {
266
+ current += char;
267
+ } else {
268
+ escaped = true;
269
+ }
270
+ continue;
271
+ }
272
+
273
+ if (char === "'" && !inDoubleQuote) {
274
+ inSingleQuote = !inSingleQuote;
275
+ continue;
276
+ }
277
+
278
+ if (char === '"' && !inSingleQuote) {
279
+ inDoubleQuote = !inDoubleQuote;
280
+ continue;
281
+ }
282
+
283
+ if (char === ' ' && !inSingleQuote && !inDoubleQuote) {
284
+ if (current.length > 0) {
285
+ tokens.push(current);
286
+ current = '';
287
+ }
288
+ continue;
289
+ }
290
+
291
+ current += char;
292
+ }
293
+
294
+ if (current.length > 0) {
295
+ tokens.push(current);
296
+ }
297
+
298
+ return tokens;
299
+ }
300
+
301
+ /**
302
+ * Generate a cURL command from request data.
303
+ */
304
+ export function generateCurl(request: {
305
+ method: string;
306
+ url: string;
307
+ headers?: { key: string; value: string; enabled: boolean }[];
308
+ body?: string;
309
+ bodyType?: string;
310
+ auth?: unknown;
311
+ }): string {
312
+ const parts: string[] = [`curl -X ${request.method}`];
313
+
314
+ // URL
315
+ const quotedUrl = request.url.includes(' ')
316
+ ? `'${request.url}'`
317
+ : request.url;
318
+ parts.push(quotedUrl);
319
+
320
+ // Headers
321
+ if (request.headers) {
322
+ for (const h of request.headers) {
323
+ if (h.enabled && h.key) {
324
+ parts.push(`-H '${h.key}: ${h.value}'`);
325
+ }
326
+ }
327
+ }
328
+
329
+ // Auth
330
+ if (request.auth) {
331
+ const auth = request.auth as Record<string, string>;
332
+ if (auth['type'] === 'basic') {
333
+ parts.push(`-u '${auth['username']}:${auth['password']}'`);
334
+ } else if (auth['type'] === 'bearer') {
335
+ parts.push(`-H 'Authorization: Bearer ${auth['token']}'`);
336
+ }
337
+ }
338
+
339
+ // Body
340
+ if (request.body) {
341
+ const bodyStr = request.body.replace(/'/g, "'\\''");
342
+ parts.push(`--data-raw '${bodyStr}'`);
343
+ }
344
+
345
+ return parts.join(' \\\n ');
346
+ }
@@ -0,0 +1,128 @@
1
+ import type { HttpMethod } from '@/lib/utils/validation';
2
+
3
+ export interface ImportedHarEntry {
4
+ name: string;
5
+ method: HttpMethod;
6
+ url: string;
7
+ status: number;
8
+ statusText: string;
9
+ headers: { key: string; value: string; enabled: boolean }[];
10
+ params: { key: string; value: string; enabled: boolean }[];
11
+ body?: string;
12
+ bodyType?: string;
13
+ responseHeaders: { key: string; value: string }[];
14
+ responseBody?: string;
15
+ responseStatus: number;
16
+ responseStatusText: string;
17
+ timing: number;
18
+ startedDateTime: string;
19
+ }
20
+
21
+ export interface ImportedHar {
22
+ entries: ImportedHarEntry[];
23
+ }
24
+
25
+ /**
26
+ * Import an HAR (HTTP Archive) file.
27
+ */
28
+ export function importHar(harJson: string): ImportedHar {
29
+ let parsed: unknown;
30
+ try {
31
+ parsed = JSON.parse(harJson);
32
+ } catch {
33
+ throw new Error('Invalid HAR file: must be valid JSON');
34
+ }
35
+
36
+ const har = parsed as Record<string, unknown>;
37
+ const log = har['log'] as Record<string, unknown> | undefined;
38
+
39
+ if (!log) {
40
+ throw new Error('Invalid HAR file: missing "log" object');
41
+ }
42
+
43
+ const entries = log['entries'] as Array<Record<string, unknown>> | undefined;
44
+
45
+ if (!entries || !Array.isArray(entries)) {
46
+ throw new Error('Invalid HAR file: missing "entries" array');
47
+ }
48
+
49
+ const result: ImportedHar = { entries: [] };
50
+
51
+ for (const entry of entries) {
52
+ const request = entry['request'] as Record<string, unknown> | undefined;
53
+ const response = entry['response'] as Record<string, unknown> | undefined;
54
+ const time = entry['time'] as number ?? 0;
55
+ const startedDateTime = entry['startedDateTime'] as string ?? '';
56
+
57
+ if (!request) continue;
58
+
59
+ const method = (request['method'] as string ?? 'GET').toUpperCase() as HttpMethod;
60
+ const url = request['url'] as string ?? '';
61
+ const headers = parseHarHeaders(request['headers'] as Array<Record<string, string>>);
62
+ const params = parseHarParams(request['queryString'] as Array<Record<string, string>>);
63
+
64
+ let body: string | undefined;
65
+ let bodyType: string | undefined;
66
+
67
+ const postData = request['postData'] as Record<string, unknown> | undefined;
68
+ if (postData) {
69
+ body = postData['text'] as string ?? '';
70
+ const mimeType = postData['mimeType'] as string ?? '';
71
+ if (mimeType.includes('json')) bodyType = 'json';
72
+ else if (mimeType.includes('urlencoded')) bodyType = 'urlencoded';
73
+ else if (mimeType.includes('form-data')) bodyType = 'form';
74
+ else bodyType = 'raw';
75
+ }
76
+
77
+ const responseHeaders = parseHarResponseHeaders(response?.['headers'] as Array<Record<string, string>>);
78
+ const responseBody = response?.['content'] as Record<string, unknown>;
79
+ const responseStatus = response?.['status'] as number ?? 0;
80
+ const responseStatusText = response?.['statusText'] as string ?? '';
81
+
82
+ result.entries.push({
83
+ name: `${method} ${new URL(url).pathname}`,
84
+ method,
85
+ url,
86
+ status: responseStatus,
87
+ statusText: responseStatusText,
88
+ headers,
89
+ params,
90
+ body,
91
+ bodyType,
92
+ responseHeaders,
93
+ responseBody: responseBody?.['text'] as string,
94
+ responseStatus,
95
+ responseStatusText,
96
+ timing: time,
97
+ startedDateTime,
98
+ });
99
+ }
100
+
101
+ return result;
102
+ }
103
+
104
+ function parseHarHeaders(headers: Array<Record<string, string>> | undefined): { key: string; value: string; enabled: boolean }[] {
105
+ if (!headers || !Array.isArray(headers)) return [];
106
+ return headers.map((h) => ({
107
+ key: h['name'] ?? '',
108
+ value: h['value'] ?? '',
109
+ enabled: true,
110
+ }));
111
+ }
112
+
113
+ function parseHarParams(params: Array<Record<string, string>> | undefined): { key: string; value: string; enabled: boolean }[] {
114
+ if (!params || !Array.isArray(params)) return [];
115
+ return params.map((p) => ({
116
+ key: p['name'] ?? '',
117
+ value: p['value'] ?? '',
118
+ enabled: true,
119
+ }));
120
+ }
121
+
122
+ function parseHarResponseHeaders(headers: Array<Record<string, string>> | undefined): { key: string; value: string }[] {
123
+ if (!headers || !Array.isArray(headers)) return [];
124
+ return headers.map((h) => ({
125
+ key: h['name'] ?? '',
126
+ value: h['value'] ?? '',
127
+ }));
128
+ }