@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.
Files changed (108) hide show
  1. package/dist/cli.js +4977 -0
  2. package/package.json +7 -3
  3. package/src/__snapshots__/cli.test.ts.snap +0 -219
  4. package/src/__test__/mcp-ctx-fixture.test.ts +0 -108
  5. package/src/__test__/mcp-ctx-fixture.ts +0 -104
  6. package/src/cli.test.ts +0 -66
  7. package/src/cli.ts +0 -43
  8. package/src/commands/cliproxy/config.test.ts +0 -475
  9. package/src/commands/cliproxy/config.ts +0 -238
  10. package/src/commands/cliproxy/deploy.test.ts +0 -181
  11. package/src/commands/cliproxy/deploy.ts +0 -157
  12. package/src/commands/cliproxy/host.test.ts +0 -73
  13. package/src/commands/cliproxy/host.ts +0 -31
  14. package/src/commands/cliproxy/index.ts +0 -21
  15. package/src/commands/cliproxy/keys.test.ts +0 -326
  16. package/src/commands/cliproxy/keys.ts +0 -201
  17. package/src/commands/cliproxy/login.test.ts +0 -415
  18. package/src/commands/cliproxy/login.ts +0 -131
  19. package/src/commands/cliproxy/models.test.ts +0 -592
  20. package/src/commands/cliproxy/models.ts +0 -237
  21. package/src/commands/cliproxy/open.test.ts +0 -132
  22. package/src/commands/cliproxy/open.ts +0 -78
  23. package/src/commands/cliproxy/setup/gh.test.ts +0 -218
  24. package/src/commands/cliproxy/setup/gh.ts +0 -250
  25. package/src/commands/cliproxy/setup/preview.test.ts +0 -159
  26. package/src/commands/cliproxy/setup/preview.ts +0 -41
  27. package/src/commands/cliproxy/setup/prompts.test.ts +0 -58
  28. package/src/commands/cliproxy/setup/prompts.ts +0 -99
  29. package/src/commands/cliproxy/setup/providers.test.ts +0 -245
  30. package/src/commands/cliproxy/setup/providers.ts +0 -136
  31. package/src/commands/cliproxy/setup/smoke-test.test.ts +0 -821
  32. package/src/commands/cliproxy/setup/smoke-test.ts +0 -223
  33. package/src/commands/cliproxy/setup/templates.test.ts +0 -357
  34. package/src/commands/cliproxy/setup/templates.ts +0 -145
  35. package/src/commands/cliproxy/setup/validation.test.ts +0 -567
  36. package/src/commands/cliproxy/setup/validation.ts +0 -209
  37. package/src/commands/cliproxy/setup/workflow-analyzer.test.ts +0 -363
  38. package/src/commands/cliproxy/setup/workflow-analyzer.ts +0 -136
  39. package/src/commands/cliproxy/setup.test.ts +0 -2366
  40. package/src/commands/cliproxy/setup.ts +0 -809
  41. package/src/commands/cliproxy/shared.test.ts +0 -118
  42. package/src/commands/cliproxy/shared.ts +0 -84
  43. package/src/commands/cliproxy/status.test.ts +0 -1157
  44. package/src/commands/cliproxy/status.ts +0 -597
  45. package/src/commands/dashboard/deploy.test.ts +0 -215
  46. package/src/commands/dashboard/deploy.ts +0 -135
  47. package/src/commands/dashboard/host.test.ts +0 -79
  48. package/src/commands/dashboard/host.ts +0 -33
  49. package/src/commands/dashboard/index.ts +0 -13
  50. package/src/commands/dashboard/logs.test.ts +0 -317
  51. package/src/commands/dashboard/logs.ts +0 -180
  52. package/src/commands/dashboard/status.test.ts +0 -708
  53. package/src/commands/dashboard/status.ts +0 -293
  54. package/src/commands/gateway/backup.test.ts +0 -426
  55. package/src/commands/gateway/backup.ts +0 -205
  56. package/src/commands/gateway/deploy.test.ts +0 -491
  57. package/src/commands/gateway/deploy.ts +0 -173
  58. package/src/commands/gateway/host.test.ts +0 -89
  59. package/src/commands/gateway/host.ts +0 -33
  60. package/src/commands/gateway/index.ts +0 -17
  61. package/src/commands/gateway/logs.test.ts +0 -263
  62. package/src/commands/gateway/logs.ts +0 -160
  63. package/src/commands/gateway/restore.test.ts +0 -650
  64. package/src/commands/gateway/restore.ts +0 -308
  65. package/src/commands/gateway/status.test.ts +0 -755
  66. package/src/commands/gateway/status.ts +0 -255
  67. package/src/commands/keeweb/deploy.test.ts +0 -177
  68. package/src/commands/keeweb/deploy.ts +0 -191
  69. package/src/commands/keeweb/index.ts +0 -11
  70. package/src/commands/keeweb/open.test.ts +0 -54
  71. package/src/commands/keeweb/open.ts +0 -30
  72. package/src/commands/keeweb/status.test.ts +0 -509
  73. package/src/commands/keeweb/status.ts +0 -359
  74. package/src/commands/mcp.test.ts +0 -260
  75. package/src/commands/mcp.ts +0 -58
  76. package/src/commands/status.test.ts +0 -222
  77. package/src/commands/status.ts +0 -161
  78. package/src/commands/umami/deploy.test.ts +0 -202
  79. package/src/commands/umami/deploy.ts +0 -132
  80. package/src/commands/umami/host.test.ts +0 -79
  81. package/src/commands/umami/host.ts +0 -33
  82. package/src/commands/umami/index.ts +0 -13
  83. package/src/commands/umami/logs.test.ts +0 -191
  84. package/src/commands/umami/logs.ts +0 -163
  85. package/src/commands/umami/status.test.ts +0 -633
  86. package/src/commands/umami/status.ts +0 -285
  87. package/src/commands/vpn/client.test.ts +0 -949
  88. package/src/commands/vpn/client.ts +0 -467
  89. package/src/commands/vpn/deploy.test.ts +0 -208
  90. package/src/commands/vpn/deploy.ts +0 -143
  91. package/src/commands/vpn/host.test.ts +0 -89
  92. package/src/commands/vpn/host.ts +0 -36
  93. package/src/commands/vpn/index.ts +0 -15
  94. package/src/commands/vpn/logs.test.ts +0 -234
  95. package/src/commands/vpn/logs.ts +0 -136
  96. package/src/commands/vpn/peers.test.ts +0 -478
  97. package/src/commands/vpn/shared.test.ts +0 -454
  98. package/src/commands/vpn/shared.ts +0 -170
  99. package/src/commands/vpn/status.test.ts +0 -142
  100. package/src/commands/vpn/status.ts +0 -78
  101. package/src/conventions.test.ts +0 -1180
  102. package/src/lib/action-ctx.ts +0 -25
  103. package/src/lib/known-hosts.test.ts +0 -192
  104. package/src/lib/known-hosts.ts +0 -76
  105. package/src/lib/redact.ts +0 -21
  106. package/src/lib/ssh-identity.test.ts +0 -134
  107. package/src/lib/ssh-identity.ts +0 -76
  108. /package/{src/resources/known_hosts → dist/known_hosts-tk724cp0.} +0 -0
@@ -1,21 +0,0 @@
1
- import type {goke} from 'goke'
2
-
3
- import {registerCliproxyConfig} from './config'
4
- import {registerCliproxyDeploy} from './deploy'
5
- import {registerCliproxyKeys} from './keys'
6
- import {registerCliproxyLogin} from './login'
7
- import {registerCliproxyModels} from './models'
8
- import {registerCliproxyOpen} from './open'
9
- import {registerCliproxySetup} from './setup'
10
- import {registerCliproxyStatus} from './status'
11
-
12
- export function registerCliproxyCommands(cli: ReturnType<typeof goke>): void {
13
- registerCliproxyStatus(cli)
14
- registerCliproxyDeploy(cli)
15
- registerCliproxyConfig(cli)
16
- registerCliproxyKeys(cli)
17
- registerCliproxyLogin(cli)
18
- registerCliproxyModels(cli)
19
- registerCliproxyOpen(cli)
20
- registerCliproxySetup(cli)
21
- }
@@ -1,326 +0,0 @@
1
- import {afterEach, beforeEach, describe, expect, it} from 'bun:test'
2
-
3
- import {createCapturedCtx, expectCapturedToInclude} from '../../__test__/mcp-ctx-fixture'
4
- import {cliproxyKeysAddAction, cliproxyKeysListAction, cliproxyKeysRemoveAction, toStringArray} from './keys'
5
-
6
- const originalFetch = globalThis.fetch
7
-
8
- type FetchReplacement = (url: string, init?: RequestInit) => Promise<Response>
9
-
10
- function createFetchImplementation(handler: FetchReplacement): typeof fetch {
11
- return Object.assign(
12
- (input: string | URL | Request, init?: RequestInit) => {
13
- if (typeof input !== 'string') {
14
- throw new TypeError(`Unexpected non-string fetch input: ${String(input)}`)
15
- }
16
-
17
- return handler(input, init)
18
- },
19
- {preconnect: originalFetch.preconnect},
20
- )
21
- }
22
-
23
- describe('toStringArray', () => {
24
- it('returns string array as-is', () => {
25
- expect(toStringArray(['a', 'b', 'c'])).toEqual(['a', 'b', 'c'])
26
- })
27
-
28
- it('filters non-string items from array', () => {
29
- expect(toStringArray(['a', 1, null, 'b'])).toEqual(['a', 'b'])
30
- })
31
-
32
- it('extracts api-keys from object', () => {
33
- expect(toStringArray({'api-keys': ['x', 'y']})).toEqual(['x', 'y'])
34
- })
35
-
36
- it('extracts api_keys from object (underscore variant)', () => {
37
- expect(toStringArray({api_keys: ['x', 'y']})).toEqual(['x', 'y'])
38
- })
39
-
40
- it('returns empty array for null', () => {
41
- expect(toStringArray(null)).toEqual([])
42
- })
43
-
44
- it('returns empty array for unrecognized shape', () => {
45
- expect(toStringArray({other: 'stuff'})).toEqual([])
46
- })
47
- })
48
-
49
- describe('cliproxyKeysListAction (Mode C, Tier-2 ctx capture)', () => {
50
- beforeEach(() => {
51
- globalThis.fetch = createFetchImplementation(async () => {
52
- throw new Error('Unexpected fetch call')
53
- })
54
- })
55
-
56
- afterEach(() => {
57
- globalThis.fetch = originalFetch
58
- })
59
-
60
- it('Mode C: captures numbered list to ctx.stdout and returns key array', async () => {
61
- globalThis.fetch = createFetchImplementation(
62
- async () =>
63
- new Response(JSON.stringify(['sk-live-aaa', 'sk-live-bbb']), {
64
- status: 200,
65
- headers: {'content-type': 'application/json'},
66
- }),
67
- )
68
-
69
- const {ctx, captured} = createCapturedCtx()
70
- const result = await cliproxyKeysListAction({url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx)
71
-
72
- // Tier-2: stdout contains formatted list
73
- expect(expectCapturedToInclude(captured, 'sk-live-aaa')).toBe(true)
74
- expect(expectCapturedToInclude(captured, '1.')).toBe(true)
75
- expect(expectCapturedToInclude(captured, '2.')).toBe(true)
76
-
77
- // Mode C: action returns the parsed array
78
- expect(Array.isArray(result)).toBe(true)
79
- expect(result).toEqual(['sk-live-aaa', 'sk-live-bbb'])
80
- })
81
-
82
- it('Mode C: security warning goes to ctx.stderr', async () => {
83
- globalThis.fetch = createFetchImplementation(
84
- async () =>
85
- new Response(JSON.stringify(['sk-live-aaa']), {
86
- status: 200,
87
- headers: {'content-type': 'application/json'},
88
- }),
89
- )
90
-
91
- const {ctx, captured} = createCapturedCtx()
92
- await cliproxyKeysListAction({url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx)
93
-
94
- const stderrText = captured.stderr.join('')
95
- expect(stderrText).toContain('API keys')
96
- })
97
-
98
- it('Mode C: returns empty array and prints "No API keys configured" when list is empty', async () => {
99
- globalThis.fetch = createFetchImplementation(
100
- async () =>
101
- new Response(JSON.stringify([]), {
102
- status: 200,
103
- headers: {'content-type': 'application/json'},
104
- }),
105
- )
106
-
107
- const {ctx, captured} = createCapturedCtx()
108
- const result = await cliproxyKeysListAction({url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx)
109
-
110
- expect(expectCapturedToInclude(captured, 'No API keys configured')).toBe(true)
111
- expect(result).toEqual([])
112
- })
113
-
114
- it('Mode C: --json flag outputs JSON array to ctx.stdout', async () => {
115
- globalThis.fetch = createFetchImplementation(
116
- async () =>
117
- new Response(JSON.stringify(['sk-live-aaa']), {
118
- status: 200,
119
- headers: {'content-type': 'application/json'},
120
- }),
121
- )
122
-
123
- const {ctx, captured} = createCapturedCtx()
124
- const result = await cliproxyKeysListAction({url: 'https://cliproxy.example.com', key: 'mgmt-key', json: true}, ctx)
125
-
126
- const stdoutText = captured.stdout.join('')
127
- const parsed = JSON.parse(stdoutText)
128
- expect(parsed).toEqual(['sk-live-aaa'])
129
- expect(result).toEqual(['sk-live-aaa'])
130
- })
131
- })
132
-
133
- describe('cliproxyKeysAddAction (Mode A, positional arg, Tier-2 ctx capture)', () => {
134
- afterEach(() => {
135
- globalThis.fetch = originalFetch
136
- })
137
-
138
- it('Mode A: captures success message to ctx.stdout after adding key', async () => {
139
- let putCalled = false
140
- globalThis.fetch = createFetchImplementation(async (_url, init) => {
141
- if (init?.method === 'PUT') {
142
- putCalled = true
143
- return new Response(JSON.stringify(['sk-live-existing', 'sk-live-new']), {
144
- status: 200,
145
- headers: {'content-type': 'application/json'},
146
- })
147
- }
148
-
149
- // GET current keys
150
- return new Response(JSON.stringify(['sk-live-existing']), {
151
- status: 200,
152
- headers: {'content-type': 'application/json'},
153
- })
154
- })
155
-
156
- const {ctx, captured} = createCapturedCtx()
157
- await cliproxyKeysAddAction('sk-live-new', {url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx)
158
-
159
- expect(putCalled).toBe(true)
160
- expect(expectCapturedToInclude(captured, 'sk-live-new')).toBe(true)
161
- expect(expectCapturedToInclude(captured, 'Added key')).toBe(true)
162
- })
163
-
164
- it('Mode A: skips PUT when key already present', async () => {
165
- let putCalled = false
166
- globalThis.fetch = createFetchImplementation(async (_url, init) => {
167
- if (init?.method === 'PUT') {
168
- putCalled = true
169
- return new Response('{}', {status: 200})
170
- }
171
-
172
- return new Response(JSON.stringify(['sk-live-existing']), {
173
- status: 200,
174
- headers: {'content-type': 'application/json'},
175
- })
176
- })
177
-
178
- const {ctx, captured} = createCapturedCtx()
179
- await cliproxyKeysAddAction('sk-live-existing', {url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx)
180
-
181
- expect(putCalled).toBe(false)
182
- expect(expectCapturedToInclude(captured, 'already present')).toBe(true)
183
- })
184
- })
185
-
186
- describe('cliproxyKeysRemoveAction (Mode A, positional arg, Tier-2 ctx capture)', () => {
187
- afterEach(() => {
188
- globalThis.fetch = originalFetch
189
- })
190
-
191
- it('Mode A: captures DELETE response to ctx.stdout', async () => {
192
- globalThis.fetch = createFetchImplementation(
193
- async () =>
194
- new Response(JSON.stringify({removed: true}), {
195
- status: 200,
196
- headers: {'content-type': 'application/json'},
197
- }),
198
- )
199
-
200
- const {ctx, captured} = createCapturedCtx()
201
- await cliproxyKeysRemoveAction('sk-live-old', {url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx)
202
-
203
- expect(expectCapturedToInclude(captured, 'removed')).toBe(true)
204
- })
205
- })
206
-
207
- describe('cliproxyKeysListAction (Tier-2 failure-path parity)', () => {
208
- const originalManagementKey = process.env.CLIPROXY_MANAGEMENT_KEY
209
-
210
- afterEach(() => {
211
- globalThis.fetch = originalFetch
212
- if (originalManagementKey === undefined) {
213
- delete process.env.CLIPROXY_MANAGEMENT_KEY
214
- } else {
215
- process.env.CLIPROXY_MANAGEMENT_KEY = originalManagementKey
216
- }
217
- })
218
-
219
- it('Tier-2: missing CLIPROXY_MANAGEMENT_KEY routes to ctx.console.error + exit(1)', async () => {
220
- delete process.env.CLIPROXY_MANAGEMENT_KEY
221
-
222
- const {ctx, captured} = createCapturedCtx()
223
- await expect(cliproxyKeysListAction({url: 'https://cliproxy.example.com'}, ctx)).rejects.toMatchObject({
224
- name: 'MockProcessExit',
225
- code: 1,
226
- })
227
- expect(captured.stderr.join('')).toContain('Management API key')
228
- expect(captured.exit).toEqual({code: 1})
229
- })
230
-
231
- it('Tier-2: HTTP 500 response routes to ctx.console.error + exit(1)', async () => {
232
- globalThis.fetch = createFetchImplementation(async () => new Response('server error', {status: 500}))
233
-
234
- const {ctx, captured} = createCapturedCtx()
235
- await expect(
236
- cliproxyKeysListAction({url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx),
237
- ).rejects.toMatchObject({
238
- name: 'MockProcessExit',
239
- code: 1,
240
- })
241
- expect(captured.stderr.join('')).toContain('HTTP 500')
242
- expect(captured.exit).toEqual({code: 1})
243
- })
244
- })
245
-
246
- describe('cliproxyKeysAddAction (Tier-2 failure-path parity)', () => {
247
- const originalManagementKey = process.env.CLIPROXY_MANAGEMENT_KEY
248
-
249
- afterEach(() => {
250
- globalThis.fetch = originalFetch
251
- if (originalManagementKey === undefined) {
252
- delete process.env.CLIPROXY_MANAGEMENT_KEY
253
- } else {
254
- process.env.CLIPROXY_MANAGEMENT_KEY = originalManagementKey
255
- }
256
- })
257
-
258
- it('Tier-2: missing CLIPROXY_MANAGEMENT_KEY routes to ctx.console.error + exit(1)', async () => {
259
- delete process.env.CLIPROXY_MANAGEMENT_KEY
260
-
261
- const {ctx, captured} = createCapturedCtx()
262
- await expect(
263
- cliproxyKeysAddAction('sk-live-new', {url: 'https://cliproxy.example.com'}, ctx),
264
- ).rejects.toMatchObject({
265
- name: 'MockProcessExit',
266
- code: 1,
267
- })
268
- expect(captured.stderr.join('')).toContain('Management API key')
269
- expect(captured.exit).toEqual({code: 1})
270
- })
271
-
272
- it('Tier-2: HTTP 500 on GET routes to ctx.console.error + exit(1)', async () => {
273
- globalThis.fetch = createFetchImplementation(async () => new Response('server error', {status: 500}))
274
-
275
- const {ctx, captured} = createCapturedCtx()
276
- await expect(
277
- cliproxyKeysAddAction('sk-live-new', {url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx),
278
- ).rejects.toMatchObject({
279
- name: 'MockProcessExit',
280
- code: 1,
281
- })
282
- expect(captured.stderr.join('')).toContain('HTTP 500')
283
- expect(captured.exit).toEqual({code: 1})
284
- })
285
- })
286
-
287
- describe('cliproxyKeysRemoveAction (Tier-2 failure-path parity)', () => {
288
- const originalManagementKey = process.env.CLIPROXY_MANAGEMENT_KEY
289
-
290
- afterEach(() => {
291
- globalThis.fetch = originalFetch
292
- if (originalManagementKey === undefined) {
293
- delete process.env.CLIPROXY_MANAGEMENT_KEY
294
- } else {
295
- process.env.CLIPROXY_MANAGEMENT_KEY = originalManagementKey
296
- }
297
- })
298
-
299
- it('Tier-2: missing CLIPROXY_MANAGEMENT_KEY routes to ctx.console.error + exit(1)', async () => {
300
- delete process.env.CLIPROXY_MANAGEMENT_KEY
301
-
302
- const {ctx, captured} = createCapturedCtx()
303
- await expect(
304
- cliproxyKeysRemoveAction('sk-live-old', {url: 'https://cliproxy.example.com'}, ctx),
305
- ).rejects.toMatchObject({
306
- name: 'MockProcessExit',
307
- code: 1,
308
- })
309
- expect(captured.stderr.join('')).toContain('Management API key')
310
- expect(captured.exit).toEqual({code: 1})
311
- })
312
-
313
- it('Tier-2: HTTP 500 response routes to ctx.console.error + exit(1)', async () => {
314
- globalThis.fetch = createFetchImplementation(async () => new Response('server error', {status: 500}))
315
-
316
- const {ctx, captured} = createCapturedCtx()
317
- await expect(
318
- cliproxyKeysRemoveAction('sk-live-old', {url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx),
319
- ).rejects.toMatchObject({
320
- name: 'MockProcessExit',
321
- code: 1,
322
- })
323
- expect(captured.stderr.join('')).toContain('HTTP 500')
324
- expect(captured.exit).toEqual({code: 1})
325
- })
326
- })
@@ -1,201 +0,0 @@
1
- import type {goke} from 'goke'
2
-
3
- import type {ActionCtx} from '../../lib/action-ctx'
4
-
5
- import {z} from 'zod'
6
-
7
- import {managementHeaders, parseManagementKeyList, requestJson, toStringArray} from './shared'
8
-
9
- export {toStringArray} from './shared'
10
-
11
- /** Minimal ctx surface consumed by cliproxy keys actions. Satisfied by both GokeExecutionContext and CapturedCtx. */
12
- // ActionCtx imported from lib/action-ctx — single source of truth for action ctx shape
13
-
14
- const DEFAULT_CLIPROXY_URL = 'https://cliproxy.fro.bot'
15
-
16
- function stripTrailingSlash(value: string): string {
17
- return value.endsWith('/') ? value.slice(0, -1) : value
18
- }
19
-
20
- function resolveBaseUrl(input?: string): string {
21
- return stripTrailingSlash(input ?? process.env.CLIPROXY_URL ?? DEFAULT_CLIPROXY_URL)
22
- }
23
-
24
- function resolveManagementKey(input?: string): string {
25
- const key = input ?? process.env.CLIPROXY_MANAGEMENT_KEY
26
-
27
- if (!key) {
28
- throw new Error('Management API key is required. Pass --key or set CLIPROXY_MANAGEMENT_KEY.')
29
- }
30
-
31
- return key
32
- }
33
-
34
- export interface KeysListOptions {
35
- url?: string
36
- key?: string
37
- json?: boolean
38
- }
39
-
40
- export async function cliproxyKeysListAction(options: KeysListOptions, ctx: ActionCtx): Promise<string[]> {
41
- try {
42
- const baseUrl = resolveBaseUrl(options.url)
43
- const managementKey = resolveManagementKey(options.key)
44
- const endpoint = `${baseUrl}/v0/management/api-keys`
45
- const payload = await requestJson(endpoint, {
46
- method: 'GET',
47
- headers: managementHeaders(managementKey),
48
- })
49
-
50
- const keys = toStringArray(payload)
51
- ctx.console.error('⚠️ Output contains API keys — avoid logging or storing in shared locations')
52
-
53
- if (options.json) {
54
- ctx.console.log(JSON.stringify(keys, null, 2))
55
- } else if (keys.length === 0) {
56
- ctx.console.log('No API keys configured')
57
- } else {
58
- for (const [index, apiKey] of keys.entries()) {
59
- ctx.console.log(`${index + 1}. ${apiKey}`)
60
- }
61
- }
62
-
63
- return keys
64
- } catch (error) {
65
- const message = error instanceof Error ? error.message : String(error)
66
- ctx.console.error(message)
67
- ctx.process.exit(1)
68
- return [] // unreachable; satisfies TS that all paths return
69
- }
70
- }
71
-
72
- export interface KeysAddOptions {
73
- url?: string
74
- key?: string
75
- }
76
-
77
- export async function cliproxyKeysAddAction(
78
- apiKeyToAdd: string,
79
- options: KeysAddOptions,
80
- ctx: ActionCtx,
81
- ): Promise<void> {
82
- try {
83
- const baseUrl = resolveBaseUrl(options.url)
84
- const managementKey = resolveManagementKey(options.key)
85
- const endpoint = `${baseUrl}/v0/management/api-keys`
86
-
87
- const currentPayload = await requestJson(endpoint, {
88
- method: 'GET',
89
- headers: managementHeaders(managementKey),
90
- })
91
- // Strict parse: a malformed or unexpected GET response must fail closed before the
92
- // destructive PUT replaces the entire key list. Permissive parsing would collapse
93
- // unknown shapes to [] and overwrite existing keys.
94
- const currentKeys = parseManagementKeyList(currentPayload)
95
-
96
- if (currentKeys.includes(apiKeyToAdd)) {
97
- ctx.console.log('Key already present; no update required.')
98
- return
99
- }
100
-
101
- const nextKeys = [...currentKeys, apiKeyToAdd]
102
- await requestJson(endpoint, {
103
- method: 'PUT',
104
- headers: managementHeaders(managementKey),
105
- body: JSON.stringify(nextKeys),
106
- })
107
-
108
- ctx.console.log(`Added key "${apiKeyToAdd}". Current key count: ${nextKeys.length}.`)
109
- } catch (error) {
110
- const message = error instanceof Error ? error.message : String(error)
111
- ctx.console.error(message)
112
- ctx.process.exit(1)
113
- }
114
- }
115
-
116
- export interface KeysRemoveOptions {
117
- url?: string
118
- key?: string
119
- }
120
-
121
- export async function cliproxyKeysRemoveAction(
122
- apiKeyToRemove: string,
123
- options: KeysRemoveOptions,
124
- ctx: ActionCtx,
125
- ): Promise<void> {
126
- try {
127
- const baseUrl = resolveBaseUrl(options.url)
128
- const managementKey = resolveManagementKey(options.key)
129
- const params = new URLSearchParams({value: apiKeyToRemove})
130
- const endpoint = `${baseUrl}/v0/management/api-keys?${params.toString()}`
131
-
132
- const payload = await requestJson(endpoint, {
133
- method: 'DELETE',
134
- headers: managementHeaders(managementKey),
135
- })
136
-
137
- ctx.console.log(JSON.stringify(payload, null, 2))
138
- } catch (error) {
139
- const message = error instanceof Error ? error.message : String(error)
140
- ctx.console.error(message)
141
- ctx.process.exit(1)
142
- }
143
- }
144
-
145
- export function registerCliproxyKeys(cli: ReturnType<typeof goke>): void {
146
- cli
147
- .command('cliproxy keys list', 'List CLIProxyAPI API keys from the management API.')
148
- .option(
149
- '--url [url]',
150
- z
151
- .string()
152
- .describe(
153
- 'Base URL for CLIProxyAPI management requests. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.',
154
- ),
155
- )
156
- .option(
157
- '--key [key]',
158
- z.string().describe('Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.'),
159
- )
160
- .option('--json', 'Output raw JSON array instead of a numbered list.')
161
- .action(cliproxyKeysListAction)
162
-
163
- cli
164
- .command(
165
- 'cliproxy keys add <key>',
166
- 'Add an API key by fetching current keys, appending the value, and replacing full key set.',
167
- )
168
- .option(
169
- '--url [url]',
170
- z
171
- .string()
172
- .describe(
173
- 'Base URL for CLIProxyAPI management requests. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.',
174
- ),
175
- )
176
- .option(
177
- '--key [key]',
178
- z.string().describe('Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.'),
179
- )
180
- .example('# Add a new API key to the current key set')
181
- .example('infra cliproxy keys add sk-live-123')
182
- .action(cliproxyKeysAddAction)
183
-
184
- cli
185
- .command('cliproxy keys remove <key>', 'Remove an API key via management API endpoint query parameter.')
186
- .option(
187
- '--url [url]',
188
- z
189
- .string()
190
- .describe(
191
- 'Base URL for CLIProxyAPI management requests. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.',
192
- ),
193
- )
194
- .option(
195
- '--key [key]',
196
- z.string().describe('Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.'),
197
- )
198
- .example('# Remove an API key from CLIProxyAPI')
199
- .example('infra cliproxy keys remove sk-live-123')
200
- .action(cliproxyKeysRemoveAction)
201
- }