@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,597 +0,0 @@
1
- import type {goke} from 'goke'
2
- import type {ActionCtx} from '../../lib/action-ctx'
3
- import type {StatusSummary} from '../status'
4
-
5
- import {z} from 'zod'
6
-
7
- import {HTTP_TIMEOUT_MS, managementHeaders} from './shared'
8
-
9
- /** Minimal ctx surface consumed by cliproxy status actions. Satisfied by both GokeExecutionContext and CapturedCtx. */
10
- // ActionCtx imported from lib/action-ctx — single source of truth for action ctx shape
11
-
12
- declare const process: {
13
- env: Record<string, string | undefined>
14
- exitCode?: number
15
- }
16
-
17
- const DEFAULT_CLIPROXY_URL = 'https://cliproxy.fro.bot'
18
-
19
- type CheckLevel = 'ok' | 'warning' | 'error'
20
-
21
- interface CheckResult {
22
- title: string
23
- level: CheckLevel
24
- summary: string
25
- details?: string[]
26
- }
27
-
28
- export function levelLabel(level: CheckLevel): string {
29
- if (level === 'ok') {
30
- return 'OK'
31
- }
32
-
33
- if (level === 'warning') {
34
- return 'WARN'
35
- }
36
-
37
- return 'ERROR'
38
- }
39
-
40
- export function formatDurationMs(durationMs: number): string {
41
- return `${Math.max(0, Math.round(durationMs))}ms`
42
- }
43
-
44
- export function stripTrailingSlash(value: string): string {
45
- return value.endsWith('/') ? value.slice(0, -1) : value
46
- }
47
-
48
- async function parseJsonResponse(response: Response): Promise<unknown> {
49
- try {
50
- return await response.json()
51
- } catch {
52
- return null
53
- }
54
- }
55
-
56
- export function toNumber(value: unknown): number | null {
57
- return typeof value === 'number' && Number.isFinite(value) ? value : null
58
- }
59
-
60
- export async function checkHttpReachability(url: string, verbose: boolean): Promise<CheckResult> {
61
- const startedAt = performance.now()
62
-
63
- try {
64
- const response = await fetch(url, {
65
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
66
- })
67
- const elapsedMs = performance.now() - startedAt
68
- const details: string[] = []
69
-
70
- if (verbose) {
71
- details.push(`URL: ${url}`)
72
- details.push(`Status text: ${response.statusText || '(none)'}`)
73
- if (response.headers.get('content-type')) {
74
- details.push(`Content-Type: ${response.headers.get('content-type')}`)
75
- }
76
- }
77
-
78
- return {
79
- title: 'HTTP reachability',
80
- level: response.ok ? 'ok' : 'error',
81
- summary: `GET ${url} → ${response.status} (${formatDurationMs(elapsedMs)})`,
82
- details,
83
- }
84
- } catch (error) {
85
- const elapsedMs = performance.now() - startedAt
86
- const message = error instanceof Error ? error.message : String(error)
87
-
88
- return {
89
- title: 'HTTP reachability',
90
- level: 'error',
91
- summary: `Request failed after ${formatDurationMs(elapsedMs)}: ${message}`,
92
- details: verbose ? [`URL: ${url}`, `Timeout: ${HTTP_TIMEOUT_MS}ms`] : undefined,
93
- }
94
- }
95
- }
96
-
97
- /** Count records in a usage-queue array that look like errors/failures. Defensive: only counts when a recognizable field exists. */
98
- function countQueueErrors(records: unknown[]): number {
99
- let count = 0
100
- for (const record of records) {
101
- if (record === null || typeof record !== 'object') {
102
- continue
103
- }
104
-
105
- const rec = record as Record<string, unknown>
106
-
107
- // Status field >= 400 indicates an HTTP-level error
108
- const status = toNumber(rec.status)
109
- if (status !== null && status >= 400) {
110
- count++
111
- continue
112
- }
113
-
114
- // Explicit error/failure markers
115
- if (rec.error !== undefined || rec.failed === true || rec.failure === true) {
116
- count++
117
- }
118
- }
119
-
120
- return count
121
- }
122
-
123
- export async function checkUsageStats(baseUrl: string, key: string): Promise<CheckResult> {
124
- const endpoint = `${baseUrl}/v0/management/usage-queue?count=50`
125
-
126
- try {
127
- const response = await fetch(endpoint, {
128
- headers: managementHeaders(key),
129
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
130
- })
131
-
132
- if (response.status === 429) {
133
- return {
134
- title: 'Usage stats',
135
- level: 'warning',
136
- summary: 'Rate limited by management API (HTTP 429). Retry in a few moments.',
137
- }
138
- }
139
-
140
- if (!response.ok) {
141
- return {
142
- title: 'Usage stats',
143
- level: 'error',
144
- summary: `GET /v0/management/usage-queue failed with HTTP ${response.status}`,
145
- }
146
- }
147
-
148
- const payload = await parseJsonResponse(response)
149
-
150
- if (!Array.isArray(payload)) {
151
- return {
152
- title: 'Usage stats',
153
- level: 'warning',
154
- summary: 'Usage-queue response was not an array — cannot parse recent activity.',
155
- }
156
- }
157
-
158
- const total = payload.length
159
- const errors = countQueueErrors(payload)
160
-
161
- if (total === 0) {
162
- return {
163
- title: 'Usage stats',
164
- level: 'ok',
165
- summary: 'recent: 0 (idle)',
166
- }
167
- }
168
-
169
- return {
170
- title: 'Usage stats',
171
- level: errors > 0 ? 'warning' : 'ok',
172
- summary: errors > 0 ? `recent: ${total}, errors: ${errors}` : `recent: ${total}`,
173
- }
174
- } catch (error) {
175
- const message = error instanceof Error ? error.message : String(error)
176
- return {
177
- title: 'Usage stats',
178
- level: 'error',
179
- summary: `Unable to read usage stats: ${message}`,
180
- }
181
- }
182
- }
183
-
184
- export async function checkVersion(baseUrl: string, key: string): Promise<CheckResult> {
185
- const endpoint = `${baseUrl}/v0/management/latest-version`
186
-
187
- try {
188
- const response = await fetch(endpoint, {
189
- headers: managementHeaders(key),
190
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
191
- })
192
-
193
- if (response.status === 429) {
194
- return {
195
- title: 'Latest version',
196
- level: 'warning',
197
- summary: 'Rate limited by management API (HTTP 429). Retry in a few moments.',
198
- }
199
- }
200
-
201
- if (!response.ok) {
202
- return {
203
- title: 'Latest version',
204
- level: 'error',
205
- summary: `GET /v0/management/latest-version failed with HTTP ${response.status}`,
206
- }
207
- }
208
-
209
- const payload = await parseJsonResponse(response)
210
-
211
- if (payload && typeof payload === 'object') {
212
- const version = (payload as Record<string, unknown>)['latest-version']
213
- if (typeof version === 'string' && version.length > 0) {
214
- return {title: 'Latest version', level: 'ok', summary: version}
215
- }
216
- }
217
-
218
- return {
219
- title: 'Latest version',
220
- level: 'warning',
221
- summary: 'Response did not include a latest-version string.',
222
- }
223
- } catch (error) {
224
- const message = error instanceof Error ? error.message : String(error)
225
- return {title: 'Latest version', level: 'error', summary: `Unable to check latest version: ${message}`}
226
- }
227
- }
228
-
229
- const DEFAULT_PROVIDER_MODEL = 'claude-sonnet-4-6'
230
-
231
- export interface ProviderAuthOptions {
232
- model?: string
233
- verbose?: boolean
234
- }
235
-
236
- /**
237
- * Probe the upstream provider auth by sending a minimal chat completion request.
238
- * Returns a CheckResult — never throws.
239
- *
240
- * - 200 → ok
241
- * - 401 OR (503 with auth-unavailable body markers) → error with remediation hint
242
- * - other non-2xx → warning (don't fail status on unrelated upstream issues)
243
- * - fetch throw (timeout/network) → warning (flaky probe should not fail status)
244
- *
245
- * The apiKey is NEVER included in summary or details output.
246
- */
247
- export async function checkProviderAuth(
248
- baseUrl: string,
249
- apiKey: string,
250
- options?: ProviderAuthOptions,
251
- ): Promise<CheckResult> {
252
- const model = options?.model ?? DEFAULT_PROVIDER_MODEL
253
- const verbose = options?.verbose === true
254
- const endpoint = `${baseUrl}/v1/chat/completions`
255
-
256
- try {
257
- const response = await fetch(endpoint, {
258
- method: 'POST',
259
- headers: {
260
- authorization: `Bearer ${apiKey}`,
261
- 'content-type': 'application/json',
262
- },
263
- body: JSON.stringify({
264
- model,
265
- messages: [{role: 'user', content: 'ping'}],
266
- max_tokens: 1,
267
- }),
268
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
269
- })
270
-
271
- const details: string[] = []
272
- if (verbose) {
273
- details.push(`URL: ${endpoint}`)
274
- details.push(`Model: ${model}`)
275
- details.push(`Status: ${response.status}`)
276
- }
277
-
278
- if (response.ok) {
279
- return {
280
- title: 'Upstream provider auth (anthropic)',
281
- level: 'ok',
282
- summary: `anthropic route OK (${model})`,
283
- details: details.length > 0 ? details : undefined,
284
- }
285
- }
286
-
287
- // 401 is always an auth failure
288
- if (response.status === 401) {
289
- return {
290
- title: 'Upstream provider auth (anthropic)',
291
- level: 'error',
292
- summary: `Anthropic upstream auth unavailable (${response.status}) — run: cliproxy login claude`,
293
- details: details.length > 0 ? details : undefined,
294
- }
295
- }
296
-
297
- // 503 may be auth_unavailable — check body for known markers
298
- if (response.status === 503) {
299
- let bodyText = ''
300
- try {
301
- bodyText = await response.text()
302
- } catch {
303
- // ignore body read failure
304
- }
305
-
306
- const isAuthUnavailable = /auth_unavailable|no auth available|providers=claude/i.test(bodyText)
307
-
308
- if (isAuthUnavailable) {
309
- return {
310
- title: 'Upstream provider auth (anthropic)',
311
- level: 'error',
312
- summary: `Anthropic upstream auth unavailable (${response.status}) — run: cliproxy login claude`,
313
- details: details.length > 0 ? details : undefined,
314
- }
315
- }
316
- }
317
-
318
- // Any other non-2xx: warning (don't fail status on unrelated upstream issues)
319
- return {
320
- title: 'Upstream provider auth (anthropic)',
321
- level: 'warning',
322
- summary: `Anthropic probe returned HTTP ${response.status}`,
323
- details: details.length > 0 ? details : undefined,
324
- }
325
- } catch (error) {
326
- const message = error instanceof Error ? error.message : String(error)
327
- return {
328
- title: 'Upstream provider auth (anthropic)',
329
- level: 'warning',
330
- summary: `Anthropic probe failed: ${message}`,
331
- details: verbose ? [`URL: ${endpoint}`, `Model: ${model}`] : undefined,
332
- }
333
- }
334
- }
335
-
336
- /**
337
- * Probe the management API with a cheap auth check before issuing parallel calls.
338
- * Returns null on success, or a CheckResult describing the auth failure.
339
- * Never throws.
340
- */
341
- async function probeManagementAuth(baseUrl: string, key: string): Promise<CheckResult | null> {
342
- const endpoint = `${baseUrl}/v0/management/config`
343
-
344
- try {
345
- const response = await fetch(endpoint, {
346
- headers: managementHeaders(key),
347
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
348
- })
349
-
350
- if (response.ok) {
351
- return null
352
- }
353
-
354
- if (response.status === 403) {
355
- // Could be an IP ban — inspect body for ban-ish markers
356
- const payload = await parseJsonResponse(response)
357
- const isBanBody =
358
- payload !== null &&
359
- typeof payload === 'object' &&
360
- Object.values(payload as Record<string, unknown>).some(
361
- v => typeof v === 'string' && /\b(?:ip[- ]?)?bann?ed\b/i.test(v),
362
- )
363
-
364
- if (isBanBody) {
365
- return {
366
- title: 'Management access',
367
- level: 'error',
368
- summary: 'IP banned — stop retrying for ~30 min. Management checks skipped.',
369
- }
370
- }
371
-
372
- return {
373
- title: 'Management access',
374
- level: 'error',
375
- summary: 'Management API returned HTTP 403. Management checks skipped.',
376
- }
377
- }
378
-
379
- if (response.status === 401) {
380
- return {
381
- title: 'Management access',
382
- level: 'error',
383
- summary: 'Management API returned HTTP 401 (invalid key). Management checks skipped.',
384
- }
385
- }
386
-
387
- return {
388
- title: 'Management access',
389
- level: 'warning',
390
- summary: `Management auth probe returned HTTP ${response.status}. Management checks skipped.`,
391
- }
392
- } catch (error) {
393
- const message = error instanceof Error ? error.message : String(error)
394
- return {
395
- title: 'Management access',
396
- level: 'error',
397
- summary: `Management auth probe failed: ${message}`,
398
- }
399
- }
400
- }
401
-
402
- type ManagementChecks =
403
- | {kind: 'no-key'}
404
- | {kind: 'auth-failure'; result: CheckResult}
405
- | {kind: 'checks'; usage: CheckResult; version: CheckResult}
406
-
407
- /**
408
- * Run management checks with single probe + parallel data fetches.
409
- * Returns a discriminated union so callers handle no-key, auth-failure, and success distinctly.
410
- */
411
- async function runManagementChecks(baseUrl: string, key: string | undefined): Promise<ManagementChecks> {
412
- if (!key) {
413
- return {kind: 'no-key'}
414
- }
415
-
416
- const authFailure = await probeManagementAuth(baseUrl, key)
417
- if (authFailure !== null) {
418
- return {kind: 'auth-failure', result: authFailure}
419
- }
420
-
421
- const [usage, version] = await Promise.all([checkUsageStats(baseUrl, key), checkVersion(baseUrl, key)])
422
- return {kind: 'checks', usage, version}
423
- }
424
-
425
- function printCheckResult(result: CheckResult, ctx: ActionCtx): void {
426
- ctx.console.log(`[${levelLabel(result.level)}] ${result.title}`)
427
- ctx.console.log(` ${result.summary}`)
428
-
429
- if (result.details && result.details.length > 0) {
430
- for (const detail of result.details) {
431
- ctx.console.log(` - ${detail}`)
432
- }
433
- }
434
- }
435
-
436
- function formatCheckSummary(result: CheckResult): string {
437
- return `${levelLabel(result.level)}: ${result.summary}`
438
- }
439
-
440
- export function formatUsageSummaryLine(result: CheckResult): string | null {
441
- // v7 recent-window format: "recent: N" or "recent: N, errors: M"
442
- const recentMatch = /recent:\s*(\d+)/.exec(result.summary)
443
- if (!recentMatch) {
444
- return null
445
- }
446
- const total = Number(recentMatch[1])
447
- const errorMatch = /errors:\s*(\d+)/.exec(result.summary)
448
- const errors = errorMatch ? Number(errorMatch[1]) : 0
449
- return `Recent requests: ${total}${errors > 0 ? `, ${errors} errors` : ''}`
450
- }
451
-
452
- export async function getCliproxyStatusSummary(baseUrl: string, key: string, verbose: boolean): Promise<StatusSummary> {
453
- const normalizedBaseUrl = stripTrailingSlash(baseUrl)
454
- const [httpResult, mgmt] = await Promise.all([
455
- checkHttpReachability(`${normalizedBaseUrl}/healthz`, verbose),
456
- runManagementChecks(normalizedBaseUrl, key || undefined),
457
- ])
458
-
459
- let version: string
460
- let usageStats: string
461
-
462
- if (mgmt.kind === 'no-key') {
463
- version = '— (no key)'
464
- usageStats = '— (no key)'
465
- } else if (mgmt.kind === 'auth-failure') {
466
- const authSummary = formatCheckSummary(mgmt.result)
467
- version = authSummary
468
- usageStats = authSummary
469
- } else {
470
- version = formatCheckSummary(mgmt.version)
471
- usageStats = formatCheckSummary(mgmt.usage)
472
- }
473
-
474
- return {
475
- app: 'cliproxy',
476
- http: formatCheckSummary(httpResult),
477
- lastDeploy: '—',
478
- version,
479
- contentHash: '—',
480
- usageStats,
481
- }
482
- }
483
-
484
- export interface StatusOptions {
485
- url?: string
486
- key?: string
487
- /** Downstream API key (bearer) for the upstream provider-auth probe. Maps from --api-key CLI flag. */
488
- apiKey?: string
489
- verbose?: boolean
490
- }
491
-
492
- export async function cliproxyStatusAction(options: StatusOptions, ctx: ActionCtx): Promise<void> {
493
- let errorCount = 0
494
- try {
495
- const verbose = options.verbose === true
496
- const baseUrl = stripTrailingSlash(options.url ?? process.env.CLIPROXY_URL ?? DEFAULT_CLIPROXY_URL)
497
-
498
- // Trusted URL: the canonical configured/default host, trailing-slash-normalized.
499
- // Ambient env keys must ONLY follow to this trusted destination to prevent
500
- // secret exfiltration when an agent passes an attacker-controlled --url.
501
- const trustedUrl = stripTrailingSlash(process.env.CLIPROXY_URL ?? DEFAULT_CLIPROXY_URL)
502
- const urlIsExplicitlyOverridden = options.url !== undefined && baseUrl !== trustedUrl
503
-
504
- // An explicit --key is always honored (operator knows what they're doing).
505
- // An ambient env key is only forwarded when the resolved baseUrl is trusted.
506
- const managementKey = options.key ?? (urlIsExplicitlyOverridden ? undefined : process.env.CLIPROXY_MANAGEMENT_KEY)
507
-
508
- // Provider (downstream) API key for the upstream provider-auth probe.
509
- // Same trusted-URL guard: ambient CLIPROXY_API_KEY only follows to the trusted URL.
510
- const providerKey = options.apiKey ?? (urlIsExplicitlyOverridden ? undefined : process.env.CLIPROXY_API_KEY)
511
-
512
- ctx.console.log('CLIProxyAPI status')
513
- ctx.console.log('')
514
-
515
- const results: CheckResult[] = [await checkHttpReachability(`${baseUrl}/healthz`, verbose)]
516
-
517
- let capturedUsageResult: CheckResult | undefined
518
-
519
- const mgmt = await runManagementChecks(baseUrl, managementKey)
520
-
521
- if (mgmt.kind === 'no-key') {
522
- results.push({
523
- title: 'Management checks',
524
- level: 'warning',
525
- summary:
526
- 'CLIPROXY_MANAGEMENT_KEY is not set. Skipping usage stats and version checks. Provide --key or set env var.',
527
- })
528
- } else if (mgmt.kind === 'auth-failure') {
529
- results.push(mgmt.result)
530
- } else {
531
- capturedUsageResult = mgmt.usage
532
- results.push(mgmt.usage, mgmt.version)
533
- }
534
-
535
- // Upstream provider-auth probe — only when a downstream API key is available.
536
- if (providerKey) {
537
- results.push(await checkProviderAuth(baseUrl, providerKey, {verbose}))
538
- } else {
539
- results.push({
540
- title: 'Upstream provider auth',
541
- level: 'warning',
542
- summary: 'CLIPROXY_API_KEY not set — skipping upstream provider probe.',
543
- })
544
- }
545
-
546
- for (const result of results) {
547
- printCheckResult(result, ctx)
548
- ctx.console.log('')
549
- }
550
-
551
- errorCount = results.filter(result => result.level === 'error').length
552
- const warningCount = results.filter(result => result.level === 'warning').length
553
-
554
- ctx.console.log(`Summary: ${results.length} checks, ${errorCount} errors, ${warningCount} warnings`)
555
-
556
- if (capturedUsageResult) {
557
- const usageLine = formatUsageSummaryLine(capturedUsageResult)
558
- if (usageLine) {
559
- ctx.console.log(usageLine)
560
- }
561
- }
562
- } catch (error) {
563
- const message = error instanceof Error ? error.message : String(error)
564
- ctx.console.error(message)
565
- ctx.process.exit(1)
566
- return
567
- }
568
-
569
- if (errorCount > 0) {
570
- ctx.process.exit(1)
571
- }
572
- }
573
-
574
- export function registerCliproxyStatus(cli: ReturnType<typeof goke>): void {
575
- cli
576
- .command('cliproxy status', 'Show operational health of CLIProxyAPI and its management endpoints.')
577
- .option(
578
- '--url [url]',
579
- z
580
- .string()
581
- .describe('Base URL for CLIProxyAPI health checks. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.'),
582
- )
583
- .option(
584
- '--key [key]',
585
- z.string().describe('Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.'),
586
- )
587
- .option(
588
- '--api-key [key]',
589
- z
590
- .string()
591
- .describe(
592
- 'Downstream API key (bearer) for the upstream provider-auth probe. Falls back to CLIPROXY_API_KEY. Skipped when absent.',
593
- ),
594
- )
595
- .option('--verbose', 'Enable verbose output for all commands')
596
- .action(cliproxyStatusAction)
597
- }