incur 0.1.17 → 0.2.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/README.md +86 -0
- package/SKILL.md +42 -0
- package/dist/Cli.d.ts +23 -2
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +222 -22
- package/dist/Cli.js.map +1 -1
- package/dist/Fetch.d.ts +26 -0
- package/dist/Fetch.d.ts.map +1 -0
- package/dist/Fetch.js +150 -0
- package/dist/Fetch.js.map +1 -0
- package/dist/Openapi.d.ts +20 -0
- package/dist/Openapi.d.ts.map +1 -0
- package/dist/Openapi.js +136 -0
- package/dist/Openapi.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/src/Cli.test-d.ts +2 -2
- package/src/Cli.test.ts +205 -0
- package/src/Cli.ts +258 -23
- package/src/Fetch.test.ts +274 -0
- package/src/Fetch.ts +170 -0
- package/src/Openapi.test.ts +320 -0
- package/src/Openapi.ts +196 -0
- package/src/e2e.test.ts +175 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { app } from '../test/fixtures/hono-api.js'
|
|
4
|
+
import * as Fetch from './Fetch.js'
|
|
5
|
+
|
|
6
|
+
describe('parseArgv', () => {
|
|
7
|
+
test('bare tokens → path segments', () => {
|
|
8
|
+
const input = Fetch.parseArgv(['users', 'list'])
|
|
9
|
+
expect(input.path).toBe('/users/list')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('empty argv → root path', () => {
|
|
13
|
+
const input = Fetch.parseArgv([])
|
|
14
|
+
expect(input.path).toBe('/')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('single token', () => {
|
|
18
|
+
const input = Fetch.parseArgv(['health'])
|
|
19
|
+
expect(input.path).toBe('/health')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('default method is GET', () => {
|
|
23
|
+
const input = Fetch.parseArgv(['users'])
|
|
24
|
+
expect(input.method).toBe('GET')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('-X sets method', () => {
|
|
28
|
+
const input = Fetch.parseArgv(['users', '-X', 'POST'])
|
|
29
|
+
expect(input.method).toBe('POST')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('--method sets method', () => {
|
|
33
|
+
const input = Fetch.parseArgv(['users', '--method', 'DELETE'])
|
|
34
|
+
expect(input.method).toBe('DELETE')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('default POST when body present', () => {
|
|
38
|
+
const input = Fetch.parseArgv(['users', '--body', '{"name":"Eve"}'])
|
|
39
|
+
expect(input.method).toBe('POST')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('-d sets body', () => {
|
|
43
|
+
const input = Fetch.parseArgv(['users', '-d', '{"name":"Bob"}'])
|
|
44
|
+
expect(input.body).toBe('{"name":"Bob"}')
|
|
45
|
+
expect(input.method).toBe('POST')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('--data sets body', () => {
|
|
49
|
+
const input = Fetch.parseArgv(['users', '--data', '{"x":1}'])
|
|
50
|
+
expect(input.body).toBe('{"x":1}')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('explicit method overrides body default', () => {
|
|
54
|
+
const input = Fetch.parseArgv(['users', '-X', 'PUT', '-d', '{"name":"Bob"}'])
|
|
55
|
+
expect(input.method).toBe('PUT')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('-H sets header', () => {
|
|
59
|
+
const input = Fetch.parseArgv(['users', '-H', 'X-Api-Key: secret'])
|
|
60
|
+
expect(input.headers.get('X-Api-Key')).toBe('secret')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('--header sets header', () => {
|
|
64
|
+
const input = Fetch.parseArgv(['users', '--header', 'Authorization: Bearer tok'])
|
|
65
|
+
expect(input.headers.get('Authorization')).toBe('Bearer tok')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('multiple headers', () => {
|
|
69
|
+
const input = Fetch.parseArgv([
|
|
70
|
+
'users',
|
|
71
|
+
'-H',
|
|
72
|
+
'X-A: 1',
|
|
73
|
+
'-H',
|
|
74
|
+
'X-B: 2',
|
|
75
|
+
])
|
|
76
|
+
expect(input.headers.get('X-A')).toBe('1')
|
|
77
|
+
expect(input.headers.get('X-B')).toBe('2')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('unknown --key value → query params', () => {
|
|
81
|
+
const input = Fetch.parseArgv(['users', '--limit', '5', '--sort', 'name'])
|
|
82
|
+
expect(input.query.get('limit')).toBe('5')
|
|
83
|
+
expect(input.query.get('sort')).toBe('name')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('--key=value → query params', () => {
|
|
87
|
+
const input = Fetch.parseArgv(['users', '--limit=5'])
|
|
88
|
+
expect(input.query.get('limit')).toBe('5')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('mixed tokens, flags, and query params', () => {
|
|
92
|
+
const input = Fetch.parseArgv([
|
|
93
|
+
'users',
|
|
94
|
+
'42',
|
|
95
|
+
'--limit',
|
|
96
|
+
'5',
|
|
97
|
+
'-X',
|
|
98
|
+
'POST',
|
|
99
|
+
'-d',
|
|
100
|
+
'{"x":1}',
|
|
101
|
+
'-H',
|
|
102
|
+
'Auth: tok',
|
|
103
|
+
])
|
|
104
|
+
expect(input.path).toBe('/users/42')
|
|
105
|
+
expect(input.method).toBe('POST')
|
|
106
|
+
expect(input.body).toBe('{"x":1}')
|
|
107
|
+
expect(input.query.get('limit')).toBe('5')
|
|
108
|
+
expect(input.headers.get('Auth')).toBe('tok')
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('buildRequest', () => {
|
|
113
|
+
test('builds GET request with path', () => {
|
|
114
|
+
const req = Fetch.buildRequest(Fetch.parseArgv(['users']))
|
|
115
|
+
expect(req.method).toBe('GET')
|
|
116
|
+
expect(new URL(req.url).pathname).toBe('/users')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('builds request with query params', () => {
|
|
120
|
+
const req = Fetch.buildRequest(Fetch.parseArgv(['users', '--limit', '5']))
|
|
121
|
+
const url = new URL(req.url)
|
|
122
|
+
expect(url.searchParams.get('limit')).toBe('5')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('builds POST request with body', () => {
|
|
126
|
+
const req = Fetch.buildRequest(
|
|
127
|
+
Fetch.parseArgv(['users', '-X', 'POST', '-d', '{"name":"Bob"}']),
|
|
128
|
+
)
|
|
129
|
+
expect(req.method).toBe('POST')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('builds request with headers', () => {
|
|
133
|
+
const req = Fetch.buildRequest(
|
|
134
|
+
Fetch.parseArgv(['users', '-H', 'X-Api-Key: secret']),
|
|
135
|
+
)
|
|
136
|
+
expect(req.headers.get('X-Api-Key')).toBe('secret')
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('parseResponse', () => {
|
|
141
|
+
test('parses JSON response', async () => {
|
|
142
|
+
const res = new Response(JSON.stringify({ ok: true }), {
|
|
143
|
+
status: 200,
|
|
144
|
+
headers: { 'content-type': 'application/json' },
|
|
145
|
+
})
|
|
146
|
+
const output = await Fetch.parseResponse(res)
|
|
147
|
+
expect(output.ok).toBe(true)
|
|
148
|
+
expect(output.status).toBe(200)
|
|
149
|
+
expect(output.data).toEqual({ ok: true })
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('parses text response', async () => {
|
|
153
|
+
const res = new Response('hello world', { status: 200 })
|
|
154
|
+
const output = await Fetch.parseResponse(res)
|
|
155
|
+
expect(output.data).toBe('hello world')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('error status → ok: false', async () => {
|
|
159
|
+
const res = new Response(JSON.stringify({ message: 'not found' }), {
|
|
160
|
+
status: 404,
|
|
161
|
+
headers: { 'content-type': 'application/json' },
|
|
162
|
+
})
|
|
163
|
+
const output = await Fetch.parseResponse(res)
|
|
164
|
+
expect(output.ok).toBe(false)
|
|
165
|
+
expect(output.status).toBe(404)
|
|
166
|
+
expect(output.data).toEqual({ message: 'not found' })
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('round-trip with Hono', () => {
|
|
171
|
+
test('GET /users', async () => {
|
|
172
|
+
const input = Fetch.parseArgv(['users'])
|
|
173
|
+
const req = Fetch.buildRequest(input)
|
|
174
|
+
const res = await app.fetch(req)
|
|
175
|
+
const output = await Fetch.parseResponse(res)
|
|
176
|
+
expect(output.ok).toBe(true)
|
|
177
|
+
expect(output.data).toEqual({ users: [{ id: 1, name: 'Alice' }], limit: 10 })
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('GET /users?limit=5', async () => {
|
|
181
|
+
const input = Fetch.parseArgv(['users', '--limit', '5'])
|
|
182
|
+
const req = Fetch.buildRequest(input)
|
|
183
|
+
const res = await app.fetch(req)
|
|
184
|
+
const output = await Fetch.parseResponse(res)
|
|
185
|
+
expect(output.ok).toBe(true)
|
|
186
|
+
expect(output.data).toEqual({ users: [{ id: 1, name: 'Alice' }], limit: 5 })
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('GET /users/:id', async () => {
|
|
190
|
+
const input = Fetch.parseArgv(['users', '42'])
|
|
191
|
+
const req = Fetch.buildRequest(input)
|
|
192
|
+
const res = await app.fetch(req)
|
|
193
|
+
const output = await Fetch.parseResponse(res)
|
|
194
|
+
expect(output.ok).toBe(true)
|
|
195
|
+
expect(output.data).toEqual({ id: 42, name: 'Alice' })
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('POST /users with body', async () => {
|
|
199
|
+
const input = Fetch.parseArgv(['users', '-X', 'POST', '-d', '{"name":"Bob"}'])
|
|
200
|
+
const req = Fetch.buildRequest(input)
|
|
201
|
+
const res = await app.fetch(req)
|
|
202
|
+
const output = await Fetch.parseResponse(res)
|
|
203
|
+
expect(output.ok).toBe(true)
|
|
204
|
+
expect(output.status).toBe(201)
|
|
205
|
+
expect(output.data).toEqual({ created: true, name: 'Bob' })
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('POST /users with implicit method', async () => {
|
|
209
|
+
const input = Fetch.parseArgv(['users', '--body', '{"name":"Eve"}'])
|
|
210
|
+
const req = Fetch.buildRequest(input)
|
|
211
|
+
const res = await app.fetch(req)
|
|
212
|
+
const output = await Fetch.parseResponse(res)
|
|
213
|
+
expect(output.ok).toBe(true)
|
|
214
|
+
expect(output.data).toEqual({ created: true, name: 'Eve' })
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test('DELETE /users/:id', async () => {
|
|
218
|
+
const input = Fetch.parseArgv(['users', '1', '--method', 'DELETE'])
|
|
219
|
+
const req = Fetch.buildRequest(input)
|
|
220
|
+
const res = await app.fetch(req)
|
|
221
|
+
const output = await Fetch.parseResponse(res)
|
|
222
|
+
expect(output.ok).toBe(true)
|
|
223
|
+
expect(output.data).toEqual({ deleted: true, id: 1 })
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test('GET /health', async () => {
|
|
227
|
+
const input = Fetch.parseArgv(['health'])
|
|
228
|
+
const req = Fetch.buildRequest(input)
|
|
229
|
+
const res = await app.fetch(req)
|
|
230
|
+
const output = await Fetch.parseResponse(res)
|
|
231
|
+
expect(output.ok).toBe(true)
|
|
232
|
+
expect(output.data).toEqual({ ok: true })
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test('GET /error → 404', async () => {
|
|
236
|
+
const input = Fetch.parseArgv(['error'])
|
|
237
|
+
const req = Fetch.buildRequest(input)
|
|
238
|
+
const res = await app.fetch(req)
|
|
239
|
+
const output = await Fetch.parseResponse(res)
|
|
240
|
+
expect(output.ok).toBe(false)
|
|
241
|
+
expect(output.status).toBe(404)
|
|
242
|
+
expect(output.data).toEqual({ message: 'not found' })
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
test('GET /text → plain text', async () => {
|
|
246
|
+
const input = Fetch.parseArgv(['text'])
|
|
247
|
+
const req = Fetch.buildRequest(input)
|
|
248
|
+
const res = await app.fetch(req)
|
|
249
|
+
const output = await Fetch.parseResponse(res)
|
|
250
|
+
expect(output.ok).toBe(true)
|
|
251
|
+
expect(output.data).toBe('hello world')
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
test('custom headers pass through', async () => {
|
|
255
|
+
const input = Fetch.parseArgv(['users', '-H', 'X-Custom: hello'])
|
|
256
|
+
const req = Fetch.buildRequest(input)
|
|
257
|
+
expect(req.headers.get('X-Custom')).toBe('hello')
|
|
258
|
+
const res = await app.fetch(req)
|
|
259
|
+
const output = await Fetch.parseResponse(res)
|
|
260
|
+
expect(output.ok).toBe(true)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test('streaming NDJSON response', async () => {
|
|
264
|
+
const input = Fetch.parseArgv(['stream'])
|
|
265
|
+
const req = Fetch.buildRequest(input)
|
|
266
|
+
const res = await app.fetch(req)
|
|
267
|
+
expect(Fetch.isStreamingResponse(res)).toBe(true)
|
|
268
|
+
const chunks: unknown[] = []
|
|
269
|
+
for await (const chunk of Fetch.parseStreamingResponse(res)) {
|
|
270
|
+
chunks.push(chunk)
|
|
271
|
+
}
|
|
272
|
+
expect(chunks).toEqual([{ progress: 1 }, { progress: 2 }])
|
|
273
|
+
})
|
|
274
|
+
})
|
package/src/Fetch.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/** Structured input parsed from curl-style argv. */
|
|
2
|
+
export type FetchInput = {
|
|
3
|
+
body: string | undefined
|
|
4
|
+
headers: Headers
|
|
5
|
+
method: string
|
|
6
|
+
path: string
|
|
7
|
+
query: URLSearchParams
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Structured output from a fetch Response. */
|
|
11
|
+
export type FetchOutput = {
|
|
12
|
+
data: unknown
|
|
13
|
+
headers: Headers
|
|
14
|
+
ok: boolean
|
|
15
|
+
status: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Reserved flags consumed by the fetch gateway (not forwarded as query params). */
|
|
19
|
+
const reservedFlags = new Set(['method', 'body', 'data', 'header'])
|
|
20
|
+
const reservedShort: Record<string, string> = { X: 'method', d: 'data', H: 'header' }
|
|
21
|
+
|
|
22
|
+
/** Parses curl-style argv into a structured fetch input. */
|
|
23
|
+
export function parseArgv(argv: string[]): FetchInput {
|
|
24
|
+
const segments: string[] = []
|
|
25
|
+
const headers = new Headers()
|
|
26
|
+
const query = new URLSearchParams()
|
|
27
|
+
let method: string | undefined
|
|
28
|
+
let body: string | undefined
|
|
29
|
+
|
|
30
|
+
let i = 0
|
|
31
|
+
while (i < argv.length) {
|
|
32
|
+
const token = argv[i]!
|
|
33
|
+
|
|
34
|
+
if (token.startsWith('--')) {
|
|
35
|
+
const eqIdx = token.indexOf('=')
|
|
36
|
+
if (eqIdx !== -1) {
|
|
37
|
+
// --key=value
|
|
38
|
+
const key = token.slice(2, eqIdx)
|
|
39
|
+
const value = token.slice(eqIdx + 1)
|
|
40
|
+
if (reservedFlags.has(key)) handleReserved(key, value)
|
|
41
|
+
else query.set(key, value)
|
|
42
|
+
i++
|
|
43
|
+
} else {
|
|
44
|
+
const key = token.slice(2)
|
|
45
|
+
const value = argv[i + 1]
|
|
46
|
+
if (reservedFlags.has(key)) {
|
|
47
|
+
handleReserved(key, value!)
|
|
48
|
+
i += 2
|
|
49
|
+
} else {
|
|
50
|
+
query.set(key, value!)
|
|
51
|
+
i += 2
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} else if (token.startsWith('-') && token.length === 2) {
|
|
55
|
+
const short = token[1]!
|
|
56
|
+
const mapped = reservedShort[short]
|
|
57
|
+
const value = argv[i + 1]!
|
|
58
|
+
if (mapped) {
|
|
59
|
+
handleReserved(mapped, value)
|
|
60
|
+
i += 2
|
|
61
|
+
} else {
|
|
62
|
+
// Unknown short flag — skip (shouldn't happen in fetch context)
|
|
63
|
+
i += 2
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
segments.push(token)
|
|
67
|
+
i++
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function handleReserved(key: string, value: string) {
|
|
72
|
+
if (key === 'method') method = value.toUpperCase()
|
|
73
|
+
else if (key === 'body' || key === 'data') body = value
|
|
74
|
+
else if (key === 'header') {
|
|
75
|
+
const colonIdx = value.indexOf(':')
|
|
76
|
+
if (colonIdx !== -1) {
|
|
77
|
+
const name = value.slice(0, colonIdx).trim()
|
|
78
|
+
const val = value.slice(colonIdx + 1).trim()
|
|
79
|
+
headers.set(name, val)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
path: segments.length > 0 ? `/${segments.join('/')}` : '/',
|
|
86
|
+
method: method ?? (body !== undefined ? 'POST' : 'GET'),
|
|
87
|
+
headers,
|
|
88
|
+
body,
|
|
89
|
+
query,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Constructs a standard Request from a FetchInput. */
|
|
94
|
+
export function buildRequest(input: FetchInput): Request {
|
|
95
|
+
const url = new URL(input.path, 'http://localhost')
|
|
96
|
+
input.query.forEach((value, key) => url.searchParams.set(key, value))
|
|
97
|
+
|
|
98
|
+
const init: RequestInit = {
|
|
99
|
+
method: input.method,
|
|
100
|
+
headers: input.headers,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (input.body !== undefined) {
|
|
104
|
+
init.body = input.body
|
|
105
|
+
if (!input.headers.has('content-type'))
|
|
106
|
+
input.headers.set('content-type', 'application/json')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return new Request(url.toString(), init)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Returns true if the response body is a stream that should be consumed incrementally. */
|
|
113
|
+
export function isStreamingResponse(response: Response): boolean {
|
|
114
|
+
return response.body !== null && response.headers.get('content-type') === 'application/x-ndjson'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Parses a streaming response body as an async generator of parsed NDJSON chunks. */
|
|
118
|
+
export async function* parseStreamingResponse(
|
|
119
|
+
response: Response,
|
|
120
|
+
): AsyncGenerator<unknown, void, unknown> {
|
|
121
|
+
const reader = response.body!.getReader()
|
|
122
|
+
const decoder = new TextDecoder()
|
|
123
|
+
let buffer = ''
|
|
124
|
+
|
|
125
|
+
while (true) {
|
|
126
|
+
const { value, done } = await reader.read()
|
|
127
|
+
if (done) break
|
|
128
|
+
buffer += decoder.decode(value, { stream: true })
|
|
129
|
+
|
|
130
|
+
let newlineIdx: number
|
|
131
|
+
while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
|
|
132
|
+
const line = buffer.slice(0, newlineIdx).trim()
|
|
133
|
+
buffer = buffer.slice(newlineIdx + 1)
|
|
134
|
+
if (line.length === 0) continue
|
|
135
|
+
try {
|
|
136
|
+
yield JSON.parse(line)
|
|
137
|
+
} catch {
|
|
138
|
+
yield line
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// flush remaining buffer
|
|
144
|
+
const remaining = buffer.trim()
|
|
145
|
+
if (remaining.length > 0) {
|
|
146
|
+
try {
|
|
147
|
+
yield JSON.parse(remaining)
|
|
148
|
+
} catch {
|
|
149
|
+
yield remaining
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Parses a fetch Response into structured output. */
|
|
155
|
+
export async function parseResponse(response: Response): Promise<FetchOutput> {
|
|
156
|
+
const text = await response.text()
|
|
157
|
+
let data: unknown
|
|
158
|
+
try {
|
|
159
|
+
data = JSON.parse(text)
|
|
160
|
+
} catch {
|
|
161
|
+
data = text
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
ok: response.ok,
|
|
166
|
+
status: response.status,
|
|
167
|
+
data,
|
|
168
|
+
headers: response.headers,
|
|
169
|
+
}
|
|
170
|
+
}
|