@coderich/sandman 0.0.14 → 0.0.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderich/sandman",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "main": "index.js",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,155 @@
1
+ exports.shq = s => `'${String(s).replace(/'/g, "'\\''")}'`;
2
+
3
+ exports.toCURL = function toCURL(request, { pretty = true, redactAuth = false } = {}) {
4
+ if (!request) return '<no request>';
5
+
6
+ const {
7
+ url,
8
+ path = '',
9
+ method: rawMethod = 'GET',
10
+ headers = undefined,
11
+ params = undefined,
12
+ data = undefined,
13
+ } = request;
14
+
15
+ // Build URL (require a base url; mirror original behavior)
16
+ const base = new URL(url);
17
+ const full = new URL(path || '', base);
18
+
19
+ // Append query params (supports object, Map, URLSearchParams, string)
20
+ if (params) {
21
+ const append = (k, v) => {
22
+ if (v == null) return;
23
+ if (Array.isArray(v) || v instanceof Set) {
24
+ for (const x of v) full.searchParams.append(k, String(x));
25
+ } else {
26
+ full.searchParams.append(k, String(v));
27
+ }
28
+ };
29
+
30
+ if (typeof params === 'string') {
31
+ for (const [k, v] of new URLSearchParams(params)) append(k, v);
32
+ } else if (params instanceof URLSearchParams) {
33
+ for (const [k, v] of params.entries()) append(k, v);
34
+ } else if (params instanceof Map) {
35
+ for (const [k, v] of params.entries()) append(k, v);
36
+ } else if (typeof params === 'object') {
37
+ for (const [k, v] of Object.entries(params)) append(k, v);
38
+ }
39
+ }
40
+
41
+ const method = String(rawMethod || 'GET').toUpperCase();
42
+ const parts = ['curl', '-sS'];
43
+
44
+ // Only add -X when not GET for cleaner output
45
+ if (method !== 'GET') parts.push('-X', method);
46
+
47
+ // Normalize headers input into flat [name, value] pairs
48
+ const collectHeaders = (hdrs) => {
49
+ const out = [];
50
+ if (!hdrs) return out;
51
+
52
+ const push = (k, v) => {
53
+ if (v == null) return;
54
+ if (Array.isArray(v)) {
55
+ for (const vv of v) if (vv != null) out.push([k, String(vv)]);
56
+ } else {
57
+ out.push([k, String(v)]);
58
+ }
59
+ };
60
+
61
+ if (typeof Headers !== 'undefined' && hdrs instanceof Headers) {
62
+ hdrs.forEach((v, k) => push(k, v));
63
+ return out;
64
+ }
65
+
66
+ if (hdrs instanceof Map) {
67
+ for (const [k, v] of hdrs.entries()) push(k, v);
68
+ return out;
69
+ }
70
+
71
+ if (Array.isArray(hdrs)) {
72
+ for (const item of hdrs) {
73
+ if (!item) continue;
74
+ if (Array.isArray(item) && item.length >= 2) push(item[0], item[1]);
75
+ else if (typeof item === 'object') {
76
+ for (const [k, v] of Object.entries(item)) push(k, v);
77
+ }
78
+ }
79
+ return out;
80
+ }
81
+
82
+ if (typeof hdrs === 'object') {
83
+ for (const [k, v] of Object.entries(hdrs)) push(k, v);
84
+ }
85
+
86
+ return out;
87
+ };
88
+
89
+ const hdrPairs = collectHeaders(headers);
90
+ const seenHeaderNames = new Set(hdrPairs.map(([k]) => k.toLowerCase()));
91
+
92
+ const redactAuthValue = (val) => {
93
+ if (!redactAuth) return val;
94
+ const s = String(val);
95
+ // Preserve scheme if present (e.g., "Bearer") but redact the token
96
+ const m = s.match(/^(\S+)\s+(.+)$/);
97
+ return m ? `${m[1]} ***REDACTED***` : '***REDACTED***';
98
+ };
99
+
100
+ for (const [k, v] of hdrPairs) {
101
+ const isAuth = /^authorization$/i.test(k);
102
+ const value = isAuth ? redactAuthValue(v) : v;
103
+ parts.push('-H', exports.shq(`${k}: ${value}`));
104
+ }
105
+
106
+ // Body (skip for GET/HEAD)
107
+ const canHaveBody = method !== 'GET' && method !== 'HEAD';
108
+
109
+ const ensureHeader = (name, value) => {
110
+ const n = name.toLowerCase();
111
+ if (seenHeaderNames.has(n)) return;
112
+ seenHeaderNames.add(n);
113
+ parts.push('-H', exports.shq(`${name}: ${value}`));
114
+ };
115
+
116
+ if (canHaveBody && data != null) {
117
+ // FormData (Node 18+/WHATWG)
118
+ const hasGlobalFD = typeof FormData !== 'undefined';
119
+ const hasGlobalFile = typeof File !== 'undefined';
120
+ const isFormData = hasGlobalFD && (data instanceof FormData || (data && data[Symbol.toStringTag] === 'FormData'));
121
+
122
+ if (isFormData) {
123
+ // Use -F for each entry; if File, use @filename (best-effort)
124
+ // Note: this assumes filename points to a local file if provided.
125
+ for (const [name, val] of data.entries()) {
126
+ if (hasGlobalFile && val instanceof File) {
127
+ const fileName = val.name || 'file';
128
+ parts.push('--form', exports.shq(`${name}=@${fileName}`));
129
+ } else {
130
+ parts.push('--form', exports.shq(`${name}=${String(val)}`));
131
+ }
132
+ }
133
+ } else if (data instanceof URLSearchParams) {
134
+ ensureHeader('Content-Type', 'application/x-www-form-urlencoded');
135
+ parts.push('--data', exports.shq(data.toString()));
136
+ } else if (typeof data === 'string') {
137
+ parts.push('--data-raw', exports.shq(data));
138
+ } else if (typeof Blob !== 'undefined' && data instanceof Blob) {
139
+ // Best-effort for binary blobs
140
+ ensureHeader('Content-Type', data.type || 'application/octet-stream');
141
+ parts.push('--data-binary', exports.shq('[binary blob]'));
142
+ } else if (typeof ArrayBuffer !== 'undefined' && (data instanceof ArrayBuffer || ArrayBuffer.isView?.(data))) {
143
+ ensureHeader('Content-Type', 'application/octet-stream');
144
+ const len = data.byteLength ?? (data.buffer && data.buffer.byteLength) ?? 0;
145
+ parts.push('--data-binary', exports.shq(`[binary ${len} bytes]`));
146
+ } else if (typeof data === 'object') {
147
+ ensureHeader('Content-Type', 'application/json');
148
+ parts.push('--data-raw', exports.shq(JSON.stringify(data)));
149
+ }
150
+ }
151
+
152
+ parts.push(exports.shq(full.toString()));
153
+
154
+ return pretty ? parts.join(' \\\n ') : parts.join(' ');
155
+ };
@@ -39,49 +39,3 @@ exports.normalizeRequest = (req) => {
39
39
  const { url, ...params } = req;
40
40
  return new Request(url, params);
41
41
  };
42
-
43
- exports.shq = s => `'${String(s).replace(/'/g, "'\\''")}'`;
44
-
45
- exports.toCURL = (request, { pretty = true, redactAuth = false } = {}) => {
46
- if (!request) return '<no request>';
47
-
48
- const { url, path, method, headers, params, data } = request;
49
-
50
- const base = new URL(url);
51
- const full = new URL(path || '', base);
52
-
53
- // append query params
54
- for (const [k, v] of Object.entries(params || {})) {
55
- if (v == null) continue;
56
- Array.isArray(v) ? v.forEach(x => full.searchParams.append(k, String(x))) : full.searchParams.append(k, String(v));
57
- }
58
-
59
- const parts = ['curl', '-sS'];
60
- if (method) parts.push('-X', method.toUpperCase());
61
-
62
- // headers
63
- const hdrs = { ...headers };
64
- for (const k of Object.keys(hdrs)) {
65
- if (redactAuth && /^authorization$/i.test(k)) {
66
- hdrs[k] = hdrs[k].replace(/(?<=^.{6}).+/, '***REDACTED***');
67
- }
68
- parts.push('-H', exports.shq(`${k}: ${hdrs[k]}`));
69
- }
70
-
71
- // body (skip for GET)
72
- if (data != null && !/^GET$/i.test(method)) {
73
- if (typeof data === 'string') {
74
- parts.push('--data-raw', exports.shq(data));
75
- } else if (data instanceof URLSearchParams) {
76
- parts.push('-H', exports.shq('Content-Type: application/x-www-form-urlencoded'));
77
- parts.push('--data', exports.shq(data.toString()));
78
- } else if (typeof data === 'object') {
79
- const hasCT = Object.keys(hdrs).some(h => /^content-type$/i.test(h));
80
- if (!hasCT) parts.push('-H', exports.shq('Content-Type: application/json'));
81
- parts.push('--data-raw', exports.shq(JSON.stringify(data)));
82
- }
83
- }
84
-
85
- parts.push(exports.shq(full.toString()));
86
- return pretty ? parts.join(' \\\n ') : parts.join(' ');
87
- };
package/src/Sandman.js CHANGED
@@ -3,6 +3,7 @@ const EventEmitter = require('events');
3
3
  const { spawn } = require('child_process');
4
4
  const ReadlineService = require('./ReadlineService');
5
5
  const UtilService = require('./UtilService');
6
+ const CURLService = require('./CURLService');
6
7
  const FetchService = require('./FetchService');
7
8
  const ConfigClient = require('./ConfigClient');
8
9
 
@@ -39,18 +40,23 @@ module.exports = class Sandman extends EventEmitter {
39
40
  return this;
40
41
  }
41
42
 
42
- #run(key) {
43
+ #run(key, opts = { emit: true }) {
43
44
  const api = this.#configClient.get(key, {});
44
- if (!api?.request) return this.emit('error', { key, error: `Request "${key}" Not Found` });
45
- this.emit('api', { api, key });
45
+
46
+ if (!api?.request) {
47
+ if (opts.emit) this.emit('error', { key, error: `Request "${key}" Not Found` });
48
+ return Promise.reject(new Error(`Request "${key}" Not Found`));
49
+ }
50
+
51
+ if (opts.emit) this.emit('api', { api, key });
46
52
  const request = FetchService.normalizeRequest(api.request);
47
- this.emit('request', { request, api, key });
53
+ if (opts.emit) this.emit('request', { request, api, key });
48
54
 
49
55
  return FetchService.fetch(request).then(({ response, data }) => {
50
- this.emit('response', { response, api, key, data });
56
+ if (opts.emit) this.emit('response', { response, api, key, data });
51
57
  return { response, api, key, data };
52
58
  }).catch((error) => {
53
- this.emit('error', { key, api, error });
59
+ if (opts.emit) this.emit('error', { key, api, error });
54
60
  return Promise.reject(error);
55
61
  }).then((results) => {
56
62
  return results.response.ok ? results : Promise.reject(results);
@@ -72,7 +78,7 @@ module.exports = class Sandman extends EventEmitter {
72
78
  $: this.#configClient.raw(key),
73
79
  [key]: this.#configClient.get(key),
74
80
  }),
75
- curl: key => FetchService.toCURL(this.#configClient.get(key, {}).request),
81
+ curl: key => CURLService.toCURL(this.#configClient.get(key, {}).request),
76
82
  quit: () => process.exit(),
77
83
  }, {
78
84
  get(obj, prop, receiver) {