@marcusrbrown/infra 0.13.0 → 0.13.2
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/dist/cli.js +4977 -0
- package/package.json +7 -3
- package/src/__snapshots__/cli.test.ts.snap +0 -219
- package/src/__test__/mcp-ctx-fixture.test.ts +0 -108
- package/src/__test__/mcp-ctx-fixture.ts +0 -104
- package/src/cli.test.ts +0 -66
- package/src/cli.ts +0 -43
- package/src/commands/cliproxy/config.test.ts +0 -475
- package/src/commands/cliproxy/config.ts +0 -238
- package/src/commands/cliproxy/deploy.test.ts +0 -181
- package/src/commands/cliproxy/deploy.ts +0 -157
- package/src/commands/cliproxy/host.test.ts +0 -73
- package/src/commands/cliproxy/host.ts +0 -31
- package/src/commands/cliproxy/index.ts +0 -21
- package/src/commands/cliproxy/keys.test.ts +0 -326
- package/src/commands/cliproxy/keys.ts +0 -201
- package/src/commands/cliproxy/login.test.ts +0 -415
- package/src/commands/cliproxy/login.ts +0 -131
- package/src/commands/cliproxy/models.test.ts +0 -592
- package/src/commands/cliproxy/models.ts +0 -237
- package/src/commands/cliproxy/open.test.ts +0 -132
- package/src/commands/cliproxy/open.ts +0 -78
- package/src/commands/cliproxy/setup/gh.test.ts +0 -218
- package/src/commands/cliproxy/setup/gh.ts +0 -250
- package/src/commands/cliproxy/setup/preview.test.ts +0 -159
- package/src/commands/cliproxy/setup/preview.ts +0 -41
- package/src/commands/cliproxy/setup/prompts.test.ts +0 -58
- package/src/commands/cliproxy/setup/prompts.ts +0 -99
- package/src/commands/cliproxy/setup/providers.test.ts +0 -245
- package/src/commands/cliproxy/setup/providers.ts +0 -136
- package/src/commands/cliproxy/setup/smoke-test.test.ts +0 -821
- package/src/commands/cliproxy/setup/smoke-test.ts +0 -223
- package/src/commands/cliproxy/setup/templates.test.ts +0 -357
- package/src/commands/cliproxy/setup/templates.ts +0 -145
- package/src/commands/cliproxy/setup/validation.test.ts +0 -567
- package/src/commands/cliproxy/setup/validation.ts +0 -209
- package/src/commands/cliproxy/setup/workflow-analyzer.test.ts +0 -363
- package/src/commands/cliproxy/setup/workflow-analyzer.ts +0 -136
- package/src/commands/cliproxy/setup.test.ts +0 -2366
- package/src/commands/cliproxy/setup.ts +0 -809
- package/src/commands/cliproxy/shared.test.ts +0 -118
- package/src/commands/cliproxy/shared.ts +0 -84
- package/src/commands/cliproxy/status.test.ts +0 -1157
- package/src/commands/cliproxy/status.ts +0 -597
- package/src/commands/dashboard/deploy.test.ts +0 -215
- package/src/commands/dashboard/deploy.ts +0 -135
- package/src/commands/dashboard/host.test.ts +0 -79
- package/src/commands/dashboard/host.ts +0 -33
- package/src/commands/dashboard/index.ts +0 -13
- package/src/commands/dashboard/logs.test.ts +0 -317
- package/src/commands/dashboard/logs.ts +0 -180
- package/src/commands/dashboard/status.test.ts +0 -708
- package/src/commands/dashboard/status.ts +0 -293
- package/src/commands/gateway/backup.test.ts +0 -426
- package/src/commands/gateway/backup.ts +0 -205
- package/src/commands/gateway/deploy.test.ts +0 -491
- package/src/commands/gateway/deploy.ts +0 -173
- package/src/commands/gateway/host.test.ts +0 -89
- package/src/commands/gateway/host.ts +0 -33
- package/src/commands/gateway/index.ts +0 -17
- package/src/commands/gateway/logs.test.ts +0 -263
- package/src/commands/gateway/logs.ts +0 -160
- package/src/commands/gateway/restore.test.ts +0 -650
- package/src/commands/gateway/restore.ts +0 -308
- package/src/commands/gateway/status.test.ts +0 -755
- package/src/commands/gateway/status.ts +0 -255
- package/src/commands/keeweb/deploy.test.ts +0 -177
- package/src/commands/keeweb/deploy.ts +0 -191
- package/src/commands/keeweb/index.ts +0 -11
- package/src/commands/keeweb/open.test.ts +0 -54
- package/src/commands/keeweb/open.ts +0 -30
- package/src/commands/keeweb/status.test.ts +0 -509
- package/src/commands/keeweb/status.ts +0 -359
- package/src/commands/mcp.test.ts +0 -260
- package/src/commands/mcp.ts +0 -58
- package/src/commands/status.test.ts +0 -222
- package/src/commands/status.ts +0 -161
- package/src/commands/umami/deploy.test.ts +0 -202
- package/src/commands/umami/deploy.ts +0 -132
- package/src/commands/umami/host.test.ts +0 -79
- package/src/commands/umami/host.ts +0 -33
- package/src/commands/umami/index.ts +0 -13
- package/src/commands/umami/logs.test.ts +0 -191
- package/src/commands/umami/logs.ts +0 -163
- package/src/commands/umami/status.test.ts +0 -633
- package/src/commands/umami/status.ts +0 -285
- package/src/commands/vpn/client.test.ts +0 -949
- package/src/commands/vpn/client.ts +0 -467
- package/src/commands/vpn/deploy.test.ts +0 -208
- package/src/commands/vpn/deploy.ts +0 -143
- package/src/commands/vpn/host.test.ts +0 -89
- package/src/commands/vpn/host.ts +0 -36
- package/src/commands/vpn/index.ts +0 -15
- package/src/commands/vpn/logs.test.ts +0 -234
- package/src/commands/vpn/logs.ts +0 -136
- package/src/commands/vpn/peers.test.ts +0 -478
- package/src/commands/vpn/shared.test.ts +0 -454
- package/src/commands/vpn/shared.ts +0 -170
- package/src/commands/vpn/status.test.ts +0 -142
- package/src/commands/vpn/status.ts +0 -78
- package/src/conventions.test.ts +0 -1180
- package/src/lib/action-ctx.ts +0 -25
- package/src/lib/known-hosts.test.ts +0 -192
- package/src/lib/known-hosts.ts +0 -76
- package/src/lib/redact.ts +0 -21
- package/src/lib/ssh-identity.test.ts +0 -134
- package/src/lib/ssh-identity.ts +0 -76
- /package/{src/resources/known_hosts → dist/known_hosts-tk724cp0.} +0 -0
|
@@ -1,475 +0,0 @@
|
|
|
1
|
-
import {existsSync, statSync} from 'node:fs'
|
|
2
|
-
import {afterEach, beforeEach, describe, expect, it} from 'bun:test'
|
|
3
|
-
|
|
4
|
-
import {createCapturedCtx, expectCapturedToInclude} from '../../__test__/mcp-ctx-fixture'
|
|
5
|
-
import {
|
|
6
|
-
buildSetRequest,
|
|
7
|
-
cliproxyConfigGetAction,
|
|
8
|
-
cliproxyConfigSetAction,
|
|
9
|
-
formatConfigAsColumns,
|
|
10
|
-
parseBoolean,
|
|
11
|
-
parseNumber,
|
|
12
|
-
resolveManagementKey,
|
|
13
|
-
} from './config'
|
|
14
|
-
import {toStringArray} from './keys'
|
|
15
|
-
import {requireSshAuthSock, resolveHost} from './login'
|
|
16
|
-
|
|
17
|
-
const originalFetch = globalThis.fetch
|
|
18
|
-
|
|
19
|
-
type FetchReplacement = (url: string, init?: RequestInit) => Promise<Response>
|
|
20
|
-
|
|
21
|
-
function createFetchImplementation(handler: FetchReplacement): typeof fetch {
|
|
22
|
-
return Object.assign(
|
|
23
|
-
(input: string | URL | Request, init?: RequestInit) => {
|
|
24
|
-
if (typeof input !== 'string') {
|
|
25
|
-
throw new TypeError(`Unexpected non-string fetch input: ${String(input)}`)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return handler(input, init)
|
|
29
|
-
},
|
|
30
|
-
{preconnect: originalFetch.preconnect},
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe('cliproxy config helpers', () => {
|
|
35
|
-
describe('parseBoolean', () => {
|
|
36
|
-
it('parses true values case-insensitively', () => {
|
|
37
|
-
expect(parseBoolean('true')).toBe(true)
|
|
38
|
-
expect(parseBoolean('True')).toBe(true)
|
|
39
|
-
expect(parseBoolean('TRUE')).toBe(true)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('parses false values case-insensitively', () => {
|
|
43
|
-
expect(parseBoolean('false')).toBe(false)
|
|
44
|
-
expect(parseBoolean('False')).toBe(false)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('throws for invalid values', () => {
|
|
48
|
-
expect(() => parseBoolean('wat')).toThrow()
|
|
49
|
-
})
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
describe('parseNumber', () => {
|
|
53
|
-
it('parses integers and floats', () => {
|
|
54
|
-
expect(parseNumber('42', 'request-retry')).toBe(42)
|
|
55
|
-
expect(parseNumber('3.14', 'request-retry')).toBe(3.14)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('throws TypeError for non-numeric values', () => {
|
|
59
|
-
expect(() => parseNumber('abc', 'request-retry')).toThrow(TypeError)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('throws TypeError for NaN', () => {
|
|
63
|
-
expect(() => parseNumber('NaN', 'request-retry')).toThrow(TypeError)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('throws TypeError for Infinity', () => {
|
|
67
|
-
expect(() => parseNumber('Infinity', 'request-retry')).toThrow(TypeError)
|
|
68
|
-
})
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
describe('buildSetRequest', () => {
|
|
72
|
-
it('builds a boolean debug request with {value} body', () => {
|
|
73
|
-
const request = buildSetRequest('https://cliproxy.example.com', 'debug', 'true')
|
|
74
|
-
|
|
75
|
-
expect(request.endpoint).toBe('https://cliproxy.example.com/v0/management/debug')
|
|
76
|
-
expect(JSON.parse(request.body)).toEqual({value: true})
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('builds a numeric request-retry request with {value} body', () => {
|
|
80
|
-
const request = buildSetRequest('https://cliproxy.example.com', 'request-retry', '3')
|
|
81
|
-
|
|
82
|
-
expect(request.endpoint).toBe('https://cliproxy.example.com/v0/management/request-retry')
|
|
83
|
-
expect(JSON.parse(request.body)).toEqual({value: 3})
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('builds a string proxy-url request with {value} body', () => {
|
|
87
|
-
const request = buildSetRequest('https://cliproxy.example.com', 'proxy-url', 'https://x.com')
|
|
88
|
-
|
|
89
|
-
expect(request.endpoint).toBe('https://cliproxy.example.com/v0/management/proxy-url')
|
|
90
|
-
expect(JSON.parse(request.body)).toEqual({value: 'https://x.com'})
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('throws for unsupported fields', () => {
|
|
94
|
-
expect(() => buildSetRequest('https://cliproxy.example.com', 'provider', 'claude')).toThrow()
|
|
95
|
-
})
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
describe('resolveManagementKey', () => {
|
|
99
|
-
const originalManagementKey = process.env.CLIPROXY_MANAGEMENT_KEY
|
|
100
|
-
|
|
101
|
-
beforeEach(() => {
|
|
102
|
-
delete process.env.CLIPROXY_MANAGEMENT_KEY
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
afterEach(() => {
|
|
106
|
-
if (originalManagementKey === undefined) {
|
|
107
|
-
delete process.env.CLIPROXY_MANAGEMENT_KEY
|
|
108
|
-
} else {
|
|
109
|
-
process.env.CLIPROXY_MANAGEMENT_KEY = originalManagementKey
|
|
110
|
-
}
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('returns explicit input when provided', () => {
|
|
114
|
-
process.env.CLIPROXY_MANAGEMENT_KEY = 'env-key'
|
|
115
|
-
|
|
116
|
-
expect(resolveManagementKey('explicit-key')).toBe('explicit-key')
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('falls back to CLIPROXY_MANAGEMENT_KEY', () => {
|
|
120
|
-
process.env.CLIPROXY_MANAGEMENT_KEY = 'env-key'
|
|
121
|
-
|
|
122
|
-
expect(resolveManagementKey()).toBe('env-key')
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('throws when no key is available', () => {
|
|
126
|
-
expect(() => resolveManagementKey()).toThrow()
|
|
127
|
-
})
|
|
128
|
-
})
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
describe('formatConfigAsColumns', () => {
|
|
132
|
-
it('formats flat object as aligned key: value lines', () => {
|
|
133
|
-
const result = formatConfigAsColumns({debug: true, 'request-retry': 3, 'proxy-url': 'https://x.com'})
|
|
134
|
-
const lines = result.split('\n')
|
|
135
|
-
|
|
136
|
-
expect(lines[0]).toBe('debug : true')
|
|
137
|
-
expect(lines[1]).toBe('request-retry: 3')
|
|
138
|
-
expect(lines[2]).toBe('proxy-url : https://x.com')
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
it('serializes nested objects as JSON', () => {
|
|
142
|
-
const result = formatConfigAsColumns({nested: {a: 1}})
|
|
143
|
-
|
|
144
|
-
expect(result).toBe('nested: {"a":1}')
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('returns empty string for empty object', () => {
|
|
148
|
-
expect(formatConfigAsColumns({})).toBe('')
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('falls back to JSON.stringify for non-objects', () => {
|
|
152
|
-
expect(formatConfigAsColumns(null)).toBe('null')
|
|
153
|
-
expect(formatConfigAsColumns([1, 2])).toBe('[\n 1,\n 2\n]')
|
|
154
|
-
})
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
describe('cliproxyConfigGetAction (Mode C, Tier-2 ctx capture)', () => {
|
|
158
|
-
afterEach(() => {
|
|
159
|
-
globalThis.fetch = originalFetch
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('Mode C: captures formatted config to ctx.stdout and returns config object', async () => {
|
|
163
|
-
const mockConfig = {debug: true, 'request-retry': 3}
|
|
164
|
-
globalThis.fetch = createFetchImplementation(
|
|
165
|
-
async () =>
|
|
166
|
-
new Response(JSON.stringify(mockConfig), {
|
|
167
|
-
status: 200,
|
|
168
|
-
headers: {'content-type': 'application/json'},
|
|
169
|
-
}),
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
const {ctx, captured} = createCapturedCtx()
|
|
173
|
-
const result = await cliproxyConfigGetAction({url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx)
|
|
174
|
-
|
|
175
|
-
// Tier-2: stdout contains formatted config
|
|
176
|
-
expect(expectCapturedToInclude(captured, 'debug')).toBe(true)
|
|
177
|
-
expect(expectCapturedToInclude(captured, 'request-retry')).toBe(true)
|
|
178
|
-
|
|
179
|
-
// Mode C: action returns the config object
|
|
180
|
-
expect(result).toEqual(mockConfig)
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
it('Mode C: security warning goes to ctx.stderr', async () => {
|
|
184
|
-
const mockConfig = {debug: false}
|
|
185
|
-
globalThis.fetch = createFetchImplementation(
|
|
186
|
-
async () =>
|
|
187
|
-
new Response(JSON.stringify(mockConfig), {
|
|
188
|
-
status: 200,
|
|
189
|
-
headers: {'content-type': 'application/json'},
|
|
190
|
-
}),
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
const {ctx, captured} = createCapturedCtx()
|
|
194
|
-
await cliproxyConfigGetAction({url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx)
|
|
195
|
-
|
|
196
|
-
const stderrText = captured.stderr.join('')
|
|
197
|
-
expect(stderrText).toContain('API keys')
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
it('Mode C: --json flag outputs raw JSON to ctx.stdout', async () => {
|
|
201
|
-
const mockConfig = {debug: true}
|
|
202
|
-
globalThis.fetch = createFetchImplementation(
|
|
203
|
-
async () =>
|
|
204
|
-
new Response(JSON.stringify(mockConfig), {
|
|
205
|
-
status: 200,
|
|
206
|
-
headers: {'content-type': 'application/json'},
|
|
207
|
-
}),
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
const {ctx, captured} = createCapturedCtx()
|
|
211
|
-
const result = await cliproxyConfigGetAction(
|
|
212
|
-
{url: 'https://cliproxy.example.com', key: 'mgmt-key', json: true},
|
|
213
|
-
ctx,
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
const stdoutText = captured.stdout.join('')
|
|
217
|
-
expect(JSON.parse(stdoutText)).toEqual(mockConfig)
|
|
218
|
-
expect(result).toEqual(mockConfig)
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
it('Mode C: --output writes file and prints confirmation to ctx.stdout', async () => {
|
|
222
|
-
const testFile = '/tmp/test-cliproxy-config-get-action.json'
|
|
223
|
-
const mockConfig = {debug: true, 'api-keys': ['key1', 'key2']}
|
|
224
|
-
globalThis.fetch = createFetchImplementation(
|
|
225
|
-
async () =>
|
|
226
|
-
new Response(JSON.stringify(mockConfig), {
|
|
227
|
-
status: 200,
|
|
228
|
-
headers: {'content-type': 'application/json'},
|
|
229
|
-
}),
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
const {ctx, captured} = createCapturedCtx()
|
|
233
|
-
try {
|
|
234
|
-
const result = await cliproxyConfigGetAction(
|
|
235
|
-
{url: 'https://cliproxy.example.com', key: 'mgmt-key', output: testFile},
|
|
236
|
-
ctx,
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
expect(existsSync(testFile)).toBe(true)
|
|
240
|
-
const {mode} = statSync(testFile)
|
|
241
|
-
expect(mode & 0o777).toBe(0o600)
|
|
242
|
-
expect(JSON.parse(await Bun.file(testFile).text())).toEqual(mockConfig)
|
|
243
|
-
expect(expectCapturedToInclude(captured, '✓ Config written to')).toBe(true)
|
|
244
|
-
expect(result).toEqual(mockConfig)
|
|
245
|
-
} finally {
|
|
246
|
-
if (existsSync(testFile)) {
|
|
247
|
-
const fs = await import('node:fs/promises')
|
|
248
|
-
await fs.unlink(testFile).catch(() => {})
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
})
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
describe('cliproxyConfigSetAction (Mode A, two positional args, Tier-2 ctx capture)', () => {
|
|
255
|
-
afterEach(() => {
|
|
256
|
-
globalThis.fetch = originalFetch
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
it('Mode A: captures PUT response to ctx.stdout', async () => {
|
|
260
|
-
globalThis.fetch = createFetchImplementation(
|
|
261
|
-
async () =>
|
|
262
|
-
new Response(JSON.stringify({value: true}), {
|
|
263
|
-
status: 200,
|
|
264
|
-
headers: {'content-type': 'application/json'},
|
|
265
|
-
}),
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
const {ctx, captured} = createCapturedCtx()
|
|
269
|
-
await cliproxyConfigSetAction('debug', 'true', {url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx)
|
|
270
|
-
|
|
271
|
-
expect(expectCapturedToInclude(captured, 'value')).toBe(true)
|
|
272
|
-
expect(expectCapturedToInclude(captured, 'true')).toBe(true)
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
it('Mode A: routes unsupported field error through ctx.console.error + exit(1)', async () => {
|
|
276
|
-
const {ctx, captured} = createCapturedCtx()
|
|
277
|
-
await expect(
|
|
278
|
-
cliproxyConfigSetAction('unsupported-field', 'val', {url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx),
|
|
279
|
-
).rejects.toMatchObject({name: 'MockProcessExit', code: 1})
|
|
280
|
-
expect(captured.stderr.join('')).toContain('not a supported mutable field')
|
|
281
|
-
expect(captured.exit).toEqual({code: 1})
|
|
282
|
-
})
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
describe('cliproxyConfigGetAction (Tier-2 failure-path parity)', () => {
|
|
286
|
-
const originalManagementKey = process.env.CLIPROXY_MANAGEMENT_KEY
|
|
287
|
-
|
|
288
|
-
afterEach(() => {
|
|
289
|
-
globalThis.fetch = originalFetch
|
|
290
|
-
if (originalManagementKey === undefined) {
|
|
291
|
-
delete process.env.CLIPROXY_MANAGEMENT_KEY
|
|
292
|
-
} else {
|
|
293
|
-
process.env.CLIPROXY_MANAGEMENT_KEY = originalManagementKey
|
|
294
|
-
}
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
it('Tier-2: missing CLIPROXY_MANAGEMENT_KEY routes to ctx.console.error + exit(1)', async () => {
|
|
298
|
-
delete process.env.CLIPROXY_MANAGEMENT_KEY
|
|
299
|
-
|
|
300
|
-
const {ctx, captured} = createCapturedCtx()
|
|
301
|
-
await expect(cliproxyConfigGetAction({url: 'https://cliproxy.example.com'}, ctx)).rejects.toMatchObject({
|
|
302
|
-
name: 'MockProcessExit',
|
|
303
|
-
code: 1,
|
|
304
|
-
})
|
|
305
|
-
expect(captured.stderr.join('')).toContain('Management API key')
|
|
306
|
-
expect(captured.exit).toEqual({code: 1})
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
it('Tier-2: HTTP 500 response routes to ctx.console.error + exit(1)', async () => {
|
|
310
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('server error', {status: 500}))
|
|
311
|
-
|
|
312
|
-
const {ctx, captured} = createCapturedCtx()
|
|
313
|
-
await expect(
|
|
314
|
-
cliproxyConfigGetAction({url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx),
|
|
315
|
-
).rejects.toMatchObject({
|
|
316
|
-
name: 'MockProcessExit',
|
|
317
|
-
code: 1,
|
|
318
|
-
})
|
|
319
|
-
expect(captured.stderr.join('')).toContain('HTTP 500')
|
|
320
|
-
expect(captured.exit).toEqual({code: 1})
|
|
321
|
-
})
|
|
322
|
-
|
|
323
|
-
it('Tier-2: --output write failure routes to ctx.console.error + exit(1)', async () => {
|
|
324
|
-
const mockConfig = {debug: true}
|
|
325
|
-
globalThis.fetch = createFetchImplementation(
|
|
326
|
-
async () =>
|
|
327
|
-
new Response(JSON.stringify(mockConfig), {
|
|
328
|
-
status: 200,
|
|
329
|
-
headers: {'content-type': 'application/json'},
|
|
330
|
-
}),
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
// Use a path that cannot be written (directory as file path)
|
|
334
|
-
const {ctx, captured} = createCapturedCtx()
|
|
335
|
-
await expect(
|
|
336
|
-
cliproxyConfigGetAction(
|
|
337
|
-
{url: 'https://cliproxy.example.com', key: 'mgmt-key', output: '/dev/null/cannot-write'},
|
|
338
|
-
ctx,
|
|
339
|
-
),
|
|
340
|
-
).rejects.toMatchObject({
|
|
341
|
-
name: 'MockProcessExit',
|
|
342
|
-
code: 1,
|
|
343
|
-
})
|
|
344
|
-
expect(captured.stderr.join('')).toContain('Failed to write config')
|
|
345
|
-
expect(captured.exit).toEqual({code: 1})
|
|
346
|
-
})
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
describe('cliproxyConfigSetAction (Tier-2 failure-path parity)', () => {
|
|
350
|
-
const originalManagementKey = process.env.CLIPROXY_MANAGEMENT_KEY
|
|
351
|
-
|
|
352
|
-
afterEach(() => {
|
|
353
|
-
globalThis.fetch = originalFetch
|
|
354
|
-
if (originalManagementKey === undefined) {
|
|
355
|
-
delete process.env.CLIPROXY_MANAGEMENT_KEY
|
|
356
|
-
} else {
|
|
357
|
-
process.env.CLIPROXY_MANAGEMENT_KEY = originalManagementKey
|
|
358
|
-
}
|
|
359
|
-
})
|
|
360
|
-
|
|
361
|
-
it('Tier-2: missing CLIPROXY_MANAGEMENT_KEY routes to ctx.console.error + exit(1)', async () => {
|
|
362
|
-
delete process.env.CLIPROXY_MANAGEMENT_KEY
|
|
363
|
-
|
|
364
|
-
const {ctx, captured} = createCapturedCtx()
|
|
365
|
-
await expect(
|
|
366
|
-
cliproxyConfigSetAction('debug', 'true', {url: 'https://cliproxy.example.com'}, ctx),
|
|
367
|
-
).rejects.toMatchObject({
|
|
368
|
-
name: 'MockProcessExit',
|
|
369
|
-
code: 1,
|
|
370
|
-
})
|
|
371
|
-
expect(captured.stderr.join('')).toContain('Management API key')
|
|
372
|
-
expect(captured.exit).toEqual({code: 1})
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
it('Tier-2: HTTP 500 response routes to ctx.console.error + exit(1)', async () => {
|
|
376
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('server error', {status: 500}))
|
|
377
|
-
|
|
378
|
-
const {ctx, captured} = createCapturedCtx()
|
|
379
|
-
await expect(
|
|
380
|
-
cliproxyConfigSetAction('debug', 'true', {url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx),
|
|
381
|
-
).rejects.toMatchObject({
|
|
382
|
-
name: 'MockProcessExit',
|
|
383
|
-
code: 1,
|
|
384
|
-
})
|
|
385
|
-
expect(captured.stderr.join('')).toContain('HTTP 500')
|
|
386
|
-
expect(captured.exit).toEqual({code: 1})
|
|
387
|
-
})
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
describe('cliproxy keys helpers', () => {
|
|
391
|
-
describe('toStringArray', () => {
|
|
392
|
-
it('returns string arrays filtered to strings only', () => {
|
|
393
|
-
expect(toStringArray(['a', 'b'])).toEqual(['a', 'b'])
|
|
394
|
-
expect(toStringArray(['a', 1, 'b', false])).toEqual(['a', 'b'])
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
it('reads api-keys (hyphenated) from objects', () => {
|
|
398
|
-
expect(toStringArray({'api-keys': ['a', 'b']})).toEqual(['a', 'b'])
|
|
399
|
-
})
|
|
400
|
-
|
|
401
|
-
it('reads api_keys (underscored) from objects', () => {
|
|
402
|
-
expect(toStringArray({api_keys: ['a', 'b']})).toEqual(['a', 'b'])
|
|
403
|
-
})
|
|
404
|
-
|
|
405
|
-
it('prefers api-keys over api_keys', () => {
|
|
406
|
-
expect(toStringArray({'api-keys': ['x'], api_keys: ['y']})).toEqual(['x'])
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
it('returns an empty array for unsupported payloads', () => {
|
|
410
|
-
expect(toStringArray(null)).toEqual([])
|
|
411
|
-
expect(toStringArray(undefined)).toEqual([])
|
|
412
|
-
expect(toStringArray(123)).toEqual([])
|
|
413
|
-
})
|
|
414
|
-
})
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
describe('cliproxy login helpers', () => {
|
|
418
|
-
describe('resolveHost', () => {
|
|
419
|
-
const originalHost = process.env.CLIPROXY_DOMAIN
|
|
420
|
-
|
|
421
|
-
beforeEach(() => {
|
|
422
|
-
delete process.env.CLIPROXY_DOMAIN
|
|
423
|
-
})
|
|
424
|
-
|
|
425
|
-
afterEach(() => {
|
|
426
|
-
if (originalHost === undefined) {
|
|
427
|
-
delete process.env.CLIPROXY_DOMAIN
|
|
428
|
-
} else {
|
|
429
|
-
process.env.CLIPROXY_DOMAIN = originalHost
|
|
430
|
-
}
|
|
431
|
-
})
|
|
432
|
-
|
|
433
|
-
it('returns explicit input when provided', () => {
|
|
434
|
-
process.env.CLIPROXY_DOMAIN = 'env.example.com'
|
|
435
|
-
|
|
436
|
-
expect(resolveHost('explicit.example.com')).toBe('explicit.example.com')
|
|
437
|
-
})
|
|
438
|
-
|
|
439
|
-
it('falls back to CLIPROXY_DOMAIN', () => {
|
|
440
|
-
process.env.CLIPROXY_DOMAIN = 'env.example.com'
|
|
441
|
-
|
|
442
|
-
expect(resolveHost()).toBe('env.example.com')
|
|
443
|
-
})
|
|
444
|
-
|
|
445
|
-
it('falls back to the default host', () => {
|
|
446
|
-
expect(resolveHost()).toBe('cliproxy.fro.bot')
|
|
447
|
-
})
|
|
448
|
-
})
|
|
449
|
-
|
|
450
|
-
describe('requireSshAuthSock', () => {
|
|
451
|
-
const originalSshAuthSock = process.env.SSH_AUTH_SOCK
|
|
452
|
-
|
|
453
|
-
beforeEach(() => {
|
|
454
|
-
delete process.env.SSH_AUTH_SOCK
|
|
455
|
-
})
|
|
456
|
-
|
|
457
|
-
afterEach(() => {
|
|
458
|
-
if (originalSshAuthSock === undefined) {
|
|
459
|
-
delete process.env.SSH_AUTH_SOCK
|
|
460
|
-
} else {
|
|
461
|
-
process.env.SSH_AUTH_SOCK = originalSshAuthSock
|
|
462
|
-
}
|
|
463
|
-
})
|
|
464
|
-
|
|
465
|
-
it('returns SSH_AUTH_SOCK when set', () => {
|
|
466
|
-
process.env.SSH_AUTH_SOCK = '/tmp/agent.sock'
|
|
467
|
-
|
|
468
|
-
expect(requireSshAuthSock()).toBe('/tmp/agent.sock')
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
it('throws when SSH_AUTH_SOCK is missing', () => {
|
|
472
|
-
expect(() => requireSshAuthSock()).toThrow()
|
|
473
|
-
})
|
|
474
|
-
})
|
|
475
|
-
})
|