@beast01/tcurl 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.
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/app.js +170 -0
- package/dist/cli.js +81 -0
- package/dist/components/Header.js +13 -0
- package/dist/components/Help.js +53 -0
- package/dist/components/RequestEditor.js +276 -0
- package/dist/components/RequestList.js +49 -0
- package/dist/components/ResponseViewer.js +83 -0
- package/dist/components/Spinner.js +13 -0
- package/dist/components/StatusBar.js +8 -0
- package/dist/components/TextEditor.js +90 -0
- package/dist/http/client.js +190 -0
- package/dist/lib/curlgen.js +40 -0
- package/dist/lib/format.js +41 -0
- package/dist/lib/id.js +4 -0
- package/dist/lib/kv.js +54 -0
- package/dist/storage.js +78 -0
- package/dist/theme.js +73 -0
- package/dist/types.js +9 -0
- package/package.json +54 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import https from 'node:https';
|
|
3
|
+
import { URL } from 'node:url';
|
|
4
|
+
import { Buffer } from 'node:buffer';
|
|
5
|
+
const MAX_REDIRECTS = 10;
|
|
6
|
+
/** Build the fully-resolved URL including enabled query params. */
|
|
7
|
+
export function buildUrl(config) {
|
|
8
|
+
const url = new URL(config.url);
|
|
9
|
+
for (const q of config.query) {
|
|
10
|
+
if (q.enabled && q.key)
|
|
11
|
+
url.searchParams.append(q.key, q.value);
|
|
12
|
+
}
|
|
13
|
+
return url;
|
|
14
|
+
}
|
|
15
|
+
/** Compute the outgoing headers, folding in auth and body content type. */
|
|
16
|
+
export function buildHeaders(config) {
|
|
17
|
+
const headers = {};
|
|
18
|
+
for (const h of config.headers) {
|
|
19
|
+
if (h.enabled && h.key)
|
|
20
|
+
headers[h.key] = h.value;
|
|
21
|
+
}
|
|
22
|
+
const hasHeader = (name) => Object.keys(headers).some((k) => k.toLowerCase() === name.toLowerCase());
|
|
23
|
+
// Auth
|
|
24
|
+
if (config.auth.type === 'bearer' && config.auth.token) {
|
|
25
|
+
if (!hasHeader('authorization'))
|
|
26
|
+
headers['Authorization'] = `Bearer ${config.auth.token}`;
|
|
27
|
+
}
|
|
28
|
+
else if (config.auth.type === 'basic') {
|
|
29
|
+
if (!hasHeader('authorization')) {
|
|
30
|
+
const raw = `${config.auth.token}:${config.auth.password}`;
|
|
31
|
+
headers['Authorization'] = `Basic ${Buffer.from(raw).toString('base64')}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Default content types by body mode
|
|
35
|
+
if (config.bodyMode === 'json' && !hasHeader('content-type')) {
|
|
36
|
+
headers['Content-Type'] = 'application/json';
|
|
37
|
+
}
|
|
38
|
+
else if (config.bodyMode === 'form' && !hasHeader('content-type')) {
|
|
39
|
+
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
40
|
+
}
|
|
41
|
+
if (!hasHeader('accept'))
|
|
42
|
+
headers['Accept'] = '*/*';
|
|
43
|
+
if (!hasHeader('user-agent'))
|
|
44
|
+
headers['User-Agent'] = 'tcurl/1.0';
|
|
45
|
+
return headers;
|
|
46
|
+
}
|
|
47
|
+
/** Serialize the request body according to its mode. */
|
|
48
|
+
export function buildBody(config) {
|
|
49
|
+
const methodHasBody = !['GET', 'HEAD'].includes(config.method);
|
|
50
|
+
if (!methodHasBody || config.bodyMode === 'none')
|
|
51
|
+
return undefined;
|
|
52
|
+
if (config.bodyMode === 'form') {
|
|
53
|
+
// body holds `key=value` lines; encode them.
|
|
54
|
+
const params = new URLSearchParams();
|
|
55
|
+
for (const line of config.body.split('\n')) {
|
|
56
|
+
const trimmed = line.trim();
|
|
57
|
+
if (!trimmed)
|
|
58
|
+
continue;
|
|
59
|
+
const idx = trimmed.indexOf('=');
|
|
60
|
+
if (idx === -1)
|
|
61
|
+
params.append(trimmed, '');
|
|
62
|
+
else
|
|
63
|
+
params.append(trimmed.slice(0, idx), trimmed.slice(idx + 1));
|
|
64
|
+
}
|
|
65
|
+
return Buffer.from(params.toString());
|
|
66
|
+
}
|
|
67
|
+
if (!config.body)
|
|
68
|
+
return undefined;
|
|
69
|
+
return Buffer.from(config.body);
|
|
70
|
+
}
|
|
71
|
+
function familyFromContentType(ct) {
|
|
72
|
+
const lower = ct.toLowerCase();
|
|
73
|
+
if (lower.includes('json'))
|
|
74
|
+
return 'json';
|
|
75
|
+
if (lower.includes('html'))
|
|
76
|
+
return 'html';
|
|
77
|
+
if (lower.includes('xml'))
|
|
78
|
+
return 'xml';
|
|
79
|
+
if (lower.startsWith('image/'))
|
|
80
|
+
return 'image';
|
|
81
|
+
if (lower.startsWith('text/'))
|
|
82
|
+
return 'text';
|
|
83
|
+
return 'text';
|
|
84
|
+
}
|
|
85
|
+
function doRequest(opts, redirectsLeft) {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const isHttps = opts.url.protocol === 'https:';
|
|
88
|
+
const transport = isHttps ? https : http;
|
|
89
|
+
const reqOptions = {
|
|
90
|
+
method: opts.method,
|
|
91
|
+
hostname: opts.url.hostname,
|
|
92
|
+
port: opts.url.port || (isHttps ? 443 : 80),
|
|
93
|
+
path: opts.url.pathname + opts.url.search,
|
|
94
|
+
headers: opts.headers,
|
|
95
|
+
rejectUnauthorized: opts.rejectUnauthorized,
|
|
96
|
+
signal: opts.signal,
|
|
97
|
+
};
|
|
98
|
+
const req = transport.request(reqOptions, (res) => {
|
|
99
|
+
const status = res.statusCode ?? 0;
|
|
100
|
+
// Handle redirects manually so we can honor the follow setting.
|
|
101
|
+
if (opts.followRedirects &&
|
|
102
|
+
redirectsLeft > 0 &&
|
|
103
|
+
[301, 302, 303, 307, 308].includes(status) &&
|
|
104
|
+
res.headers.location) {
|
|
105
|
+
res.resume(); // drain
|
|
106
|
+
const nextUrl = new URL(res.headers.location, opts.url);
|
|
107
|
+
// 303, or 301/302 on POST, downgrade to GET per browser behavior.
|
|
108
|
+
let nextMethod = opts.method;
|
|
109
|
+
let nextBody = opts.body;
|
|
110
|
+
if (status === 303 || ((status === 301 || status === 302) && opts.method === 'POST')) {
|
|
111
|
+
nextMethod = 'GET';
|
|
112
|
+
nextBody = undefined;
|
|
113
|
+
}
|
|
114
|
+
doRequest({ ...opts, url: nextUrl, method: nextMethod, body: nextBody }, redirectsLeft - 1).then(resolve, reject);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const chunks = [];
|
|
118
|
+
res.on('data', (c) => chunks.push(c));
|
|
119
|
+
res.on('end', () => {
|
|
120
|
+
const bodyBuf = Buffer.concat(chunks);
|
|
121
|
+
const headers = {};
|
|
122
|
+
for (const [k, v] of Object.entries(res.headers)) {
|
|
123
|
+
headers[k] = Array.isArray(v) ? v.join(', ') : String(v ?? '');
|
|
124
|
+
}
|
|
125
|
+
const ct = headers['content-type'] ?? '';
|
|
126
|
+
resolve({
|
|
127
|
+
ok: status >= 200 && status < 400,
|
|
128
|
+
status,
|
|
129
|
+
statusText: res.statusMessage ?? '',
|
|
130
|
+
headers,
|
|
131
|
+
body: bodyBuf.toString('utf8'),
|
|
132
|
+
contentType: familyFromContentType(ct),
|
|
133
|
+
sizeBytes: bodyBuf.length,
|
|
134
|
+
finalUrl: opts.url.toString(),
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
req.on('error', (err) => reject(err));
|
|
139
|
+
if (opts.body)
|
|
140
|
+
req.write(opts.body);
|
|
141
|
+
req.end();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/** Execute a request config and return a normalized result (never throws). */
|
|
145
|
+
export async function executeRequest(config) {
|
|
146
|
+
const start = Date.now();
|
|
147
|
+
const controller = new AbortController();
|
|
148
|
+
let timer;
|
|
149
|
+
if (config.timeoutMs > 0) {
|
|
150
|
+
timer = setTimeout(() => controller.abort(), config.timeoutMs);
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const url = buildUrl(config);
|
|
154
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
155
|
+
throw new Error(`Unsupported protocol: ${url.protocol}`);
|
|
156
|
+
}
|
|
157
|
+
const result = await doRequest({
|
|
158
|
+
url,
|
|
159
|
+
method: config.method,
|
|
160
|
+
headers: buildHeaders(config),
|
|
161
|
+
body: buildBody(config),
|
|
162
|
+
timeoutMs: config.timeoutMs,
|
|
163
|
+
rejectUnauthorized: config.rejectUnauthorized,
|
|
164
|
+
followRedirects: config.followRedirects,
|
|
165
|
+
signal: controller.signal,
|
|
166
|
+
}, MAX_REDIRECTS);
|
|
167
|
+
return { ...result, durationMs: Date.now() - start };
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
const aborted = err?.name === 'AbortError' || controller.signal.aborted;
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
status: 0,
|
|
174
|
+
statusText: '',
|
|
175
|
+
headers: {},
|
|
176
|
+
body: '',
|
|
177
|
+
contentType: 'text',
|
|
178
|
+
durationMs: Date.now() - start,
|
|
179
|
+
sizeBytes: 0,
|
|
180
|
+
finalUrl: config.url,
|
|
181
|
+
error: aborted
|
|
182
|
+
? `Request timed out after ${config.timeoutMs}ms`
|
|
183
|
+
: String(err?.message ?? err),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
if (timer)
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { buildUrl, buildHeaders, buildBody } from '../http/client.js';
|
|
2
|
+
function shellQuote(value) {
|
|
3
|
+
// Wrap in single quotes, escaping embedded single quotes.
|
|
4
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
5
|
+
}
|
|
6
|
+
/** Render a request config as an equivalent `curl` command line. */
|
|
7
|
+
export function toCurl(config) {
|
|
8
|
+
const parts = ['curl'];
|
|
9
|
+
if (config.method !== 'GET')
|
|
10
|
+
parts.push('-X', config.method);
|
|
11
|
+
let url;
|
|
12
|
+
try {
|
|
13
|
+
url = buildUrl(config).toString();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
url = config.url;
|
|
17
|
+
}
|
|
18
|
+
parts.push(shellQuote(url));
|
|
19
|
+
const headers = buildHeaders(config);
|
|
20
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
21
|
+
parts.push('-H', shellQuote(`${k}: ${v}`));
|
|
22
|
+
}
|
|
23
|
+
const body = buildBody(config);
|
|
24
|
+
if (body && body.length > 0) {
|
|
25
|
+
parts.push('--data', shellQuote(body.toString('utf8')));
|
|
26
|
+
}
|
|
27
|
+
if (!config.followRedirects) {
|
|
28
|
+
/* curl does not follow by default; nothing to add */
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
parts.push('-L');
|
|
32
|
+
}
|
|
33
|
+
if (!config.rejectUnauthorized)
|
|
34
|
+
parts.push('-k');
|
|
35
|
+
if (config.timeoutMs > 0) {
|
|
36
|
+
parts.push('--max-time', String(Math.ceil(config.timeoutMs / 1000)));
|
|
37
|
+
}
|
|
38
|
+
// Join with line continuations for readability.
|
|
39
|
+
return parts.join(' ');
|
|
40
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Pretty-print a body based on its detected content family. */
|
|
2
|
+
export function prettyBody(body, family) {
|
|
3
|
+
if (family === 'json') {
|
|
4
|
+
try {
|
|
5
|
+
return JSON.stringify(JSON.parse(body), null, 2);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return body;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return body;
|
|
12
|
+
}
|
|
13
|
+
export function formatBytes(bytes) {
|
|
14
|
+
if (bytes < 1024)
|
|
15
|
+
return `${bytes} B`;
|
|
16
|
+
if (bytes < 1024 * 1024)
|
|
17
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
18
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
19
|
+
}
|
|
20
|
+
export function formatDuration(ms) {
|
|
21
|
+
if (ms < 1000)
|
|
22
|
+
return `${ms} ms`;
|
|
23
|
+
return `${(ms / 1000).toFixed(2)} s`;
|
|
24
|
+
}
|
|
25
|
+
/** Split a string into wrapped lines of at most `width` columns. */
|
|
26
|
+
export function wrapLines(text, width) {
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const rawLine of text.split('\n')) {
|
|
29
|
+
if (rawLine.length <= width) {
|
|
30
|
+
out.push(rawLine);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
let line = rawLine;
|
|
34
|
+
while (line.length > width) {
|
|
35
|
+
out.push(line.slice(0, width));
|
|
36
|
+
line = line.slice(width);
|
|
37
|
+
}
|
|
38
|
+
out.push(line);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
package/dist/lib/id.js
ADDED
package/dist/lib/kv.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Render header pairs as `Key: Value` lines for text editing. */
|
|
2
|
+
export function headersToText(headers) {
|
|
3
|
+
return headers
|
|
4
|
+
.filter((h) => h.enabled)
|
|
5
|
+
.map((h) => `${h.key}: ${h.value}`)
|
|
6
|
+
.join('\n');
|
|
7
|
+
}
|
|
8
|
+
export function textToHeaders(text) {
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const line of text.split('\n')) {
|
|
11
|
+
const trimmed = line.trim();
|
|
12
|
+
if (!trimmed)
|
|
13
|
+
continue;
|
|
14
|
+
const idx = trimmed.indexOf(':');
|
|
15
|
+
if (idx === -1) {
|
|
16
|
+
out.push({ key: trimmed, value: '', enabled: true });
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
out.push({
|
|
20
|
+
key: trimmed.slice(0, idx).trim(),
|
|
21
|
+
value: trimmed.slice(idx + 1).trim(),
|
|
22
|
+
enabled: true,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
/** Render query pairs as `key=value` lines for text editing. */
|
|
29
|
+
export function queryToText(query) {
|
|
30
|
+
return query
|
|
31
|
+
.filter((q) => q.enabled)
|
|
32
|
+
.map((q) => `${q.key}=${q.value}`)
|
|
33
|
+
.join('\n');
|
|
34
|
+
}
|
|
35
|
+
export function textToQuery(text) {
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const line of text.split('\n')) {
|
|
38
|
+
const trimmed = line.trim();
|
|
39
|
+
if (!trimmed)
|
|
40
|
+
continue;
|
|
41
|
+
const idx = trimmed.indexOf('=');
|
|
42
|
+
if (idx === -1) {
|
|
43
|
+
out.push({ key: trimmed, value: '', enabled: true });
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
out.push({
|
|
47
|
+
key: trimmed.slice(0, idx).trim(),
|
|
48
|
+
value: trimmed.slice(idx + 1).trim(),
|
|
49
|
+
enabled: true,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import envPaths from 'env-paths';
|
|
4
|
+
import { newId } from './lib/id.js';
|
|
5
|
+
const paths = envPaths('tcurl', { suffix: '' });
|
|
6
|
+
const STORE_DIR = paths.config;
|
|
7
|
+
const STORE_FILE = path.join(STORE_DIR, 'store.json');
|
|
8
|
+
const STORE_VERSION = 1;
|
|
9
|
+
function emptyStore() {
|
|
10
|
+
return { version: STORE_VERSION, requests: [], theme: 'gruvbox' };
|
|
11
|
+
}
|
|
12
|
+
export function defaultRequest(partial = {}) {
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
return {
|
|
15
|
+
id: newId(),
|
|
16
|
+
name: 'Untitled request',
|
|
17
|
+
method: 'GET',
|
|
18
|
+
url: '',
|
|
19
|
+
headers: [],
|
|
20
|
+
query: [],
|
|
21
|
+
bodyMode: 'none',
|
|
22
|
+
body: '',
|
|
23
|
+
auth: { type: 'none', token: '', password: '' },
|
|
24
|
+
timeoutMs: 30000,
|
|
25
|
+
followRedirects: true,
|
|
26
|
+
rejectUnauthorized: true,
|
|
27
|
+
createdAt: now,
|
|
28
|
+
updatedAt: now,
|
|
29
|
+
...partial,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Migrate/normalize a stored request so older files stay loadable. */
|
|
33
|
+
function normalizeRequest(raw) {
|
|
34
|
+
const base = defaultRequest();
|
|
35
|
+
return {
|
|
36
|
+
...base,
|
|
37
|
+
...raw,
|
|
38
|
+
auth: { ...base.auth, ...(raw?.auth ?? {}) },
|
|
39
|
+
headers: Array.isArray(raw?.headers) ? raw.headers : [],
|
|
40
|
+
query: Array.isArray(raw?.query) ? raw.query : [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export async function loadStore() {
|
|
44
|
+
try {
|
|
45
|
+
const text = await fs.readFile(STORE_FILE, 'utf8');
|
|
46
|
+
const parsed = JSON.parse(text);
|
|
47
|
+
return {
|
|
48
|
+
version: STORE_VERSION,
|
|
49
|
+
theme: typeof parsed.theme === 'string' ? parsed.theme : 'gruvbox',
|
|
50
|
+
requests: Array.isArray(parsed.requests)
|
|
51
|
+
? parsed.requests.map(normalizeRequest)
|
|
52
|
+
: [],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
if (err?.code === 'ENOENT')
|
|
57
|
+
return emptyStore();
|
|
58
|
+
// Corrupt file: back it up and start fresh rather than crashing.
|
|
59
|
+
try {
|
|
60
|
+
await fs.rename(STORE_FILE, `${STORE_FILE}.corrupt-${Date.now()}`);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
/* ignore */
|
|
64
|
+
}
|
|
65
|
+
return emptyStore();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export async function saveStore(store) {
|
|
69
|
+
await fs.mkdir(STORE_DIR, { recursive: true });
|
|
70
|
+
const tmp = `${STORE_FILE}.tmp-${process.pid}`;
|
|
71
|
+
const data = JSON.stringify(store, null, 2);
|
|
72
|
+
// Write atomically so a crash mid-write can't corrupt the store.
|
|
73
|
+
await fs.writeFile(tmp, data, 'utf8');
|
|
74
|
+
await fs.rename(tmp, STORE_FILE);
|
|
75
|
+
}
|
|
76
|
+
export function storeLocation() {
|
|
77
|
+
return STORE_FILE;
|
|
78
|
+
}
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Gruvbox dark — https://github.com/morhetz/gruvbox */
|
|
2
|
+
export const gruvbox = {
|
|
3
|
+
name: 'gruvbox',
|
|
4
|
+
bg: '#282828',
|
|
5
|
+
fg: '#ebdbb2',
|
|
6
|
+
fgDim: '#a89984',
|
|
7
|
+
accent: '#fabd2f', // yellow
|
|
8
|
+
accentAlt: '#83a598', // blue
|
|
9
|
+
success: '#b8bb26', // green
|
|
10
|
+
warning: '#fe8019', // orange
|
|
11
|
+
error: '#fb4934', // red
|
|
12
|
+
info: '#8ec07c', // aqua
|
|
13
|
+
muted: '#665c54',
|
|
14
|
+
selectionBg: '#3c3836',
|
|
15
|
+
border: '#504945',
|
|
16
|
+
borderActive: '#fabd2f',
|
|
17
|
+
};
|
|
18
|
+
/** Ember — a warm, glowing dark palette. */
|
|
19
|
+
export const ember = {
|
|
20
|
+
name: 'ember',
|
|
21
|
+
bg: '#1b1206',
|
|
22
|
+
fg: '#f5e6c8',
|
|
23
|
+
fgDim: '#c9a36a',
|
|
24
|
+
accent: '#ff9e3b', // ember orange
|
|
25
|
+
accentAlt: '#ffcf70', // amber
|
|
26
|
+
success: '#c2d94c', // lime
|
|
27
|
+
warning: '#ffb454', // amber-orange
|
|
28
|
+
error: '#f07178', // coral
|
|
29
|
+
info: '#e6a15c', // tan
|
|
30
|
+
muted: '#6b4f2a',
|
|
31
|
+
selectionBg: '#2a1c0a',
|
|
32
|
+
border: '#4d3616',
|
|
33
|
+
borderActive: '#ff9e3b',
|
|
34
|
+
};
|
|
35
|
+
export const themes = {
|
|
36
|
+
gruvbox,
|
|
37
|
+
ember,
|
|
38
|
+
};
|
|
39
|
+
export function getTheme(name) {
|
|
40
|
+
return themes[name] ?? gruvbox;
|
|
41
|
+
}
|
|
42
|
+
/** A stable color per HTTP method for at-a-glance scanning. */
|
|
43
|
+
export function methodColor(method, theme) {
|
|
44
|
+
switch (method) {
|
|
45
|
+
case 'GET':
|
|
46
|
+
return theme.success;
|
|
47
|
+
case 'POST':
|
|
48
|
+
return theme.accent;
|
|
49
|
+
case 'PUT':
|
|
50
|
+
return theme.accentAlt;
|
|
51
|
+
case 'PATCH':
|
|
52
|
+
return theme.info;
|
|
53
|
+
case 'DELETE':
|
|
54
|
+
return theme.error;
|
|
55
|
+
case 'HEAD':
|
|
56
|
+
case 'OPTIONS':
|
|
57
|
+
return theme.fgDim;
|
|
58
|
+
default:
|
|
59
|
+
return theme.fg;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Color for an HTTP status code by class. */
|
|
63
|
+
export function statusColor(status, theme) {
|
|
64
|
+
if (status >= 200 && status < 300)
|
|
65
|
+
return theme.success;
|
|
66
|
+
if (status >= 300 && status < 400)
|
|
67
|
+
return theme.accentAlt;
|
|
68
|
+
if (status >= 400 && status < 500)
|
|
69
|
+
return theme.warning;
|
|
70
|
+
if (status >= 500)
|
|
71
|
+
return theme.error;
|
|
72
|
+
return theme.fgDim;
|
|
73
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@beast01/tcurl",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "An interactive terminal UI (TUI) for building, sending, and saving HTTP requests — curl, but comfortable.",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"bin": {
|
|
10
|
+
"tcurl": "dist/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"main": "dist/cli.js",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
24
|
+
"start": "node dist/cli.js",
|
|
25
|
+
"prepublishOnly": "npm run build",
|
|
26
|
+
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\""
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"curl",
|
|
30
|
+
"tui",
|
|
31
|
+
"terminal",
|
|
32
|
+
"http",
|
|
33
|
+
"rest",
|
|
34
|
+
"client",
|
|
35
|
+
"api",
|
|
36
|
+
"ink",
|
|
37
|
+
"cli",
|
|
38
|
+
"postman",
|
|
39
|
+
"httpie",
|
|
40
|
+
"gruvbox"
|
|
41
|
+
],
|
|
42
|
+
"author": "",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"env-paths": "^3.0.0",
|
|
46
|
+
"ink": "^5.0.1",
|
|
47
|
+
"react": "^18.3.1"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^22.20.0",
|
|
51
|
+
"@types/react": "^18.3.12",
|
|
52
|
+
"typescript": "^5.6.3"
|
|
53
|
+
}
|
|
54
|
+
}
|