@marcusrbrown/infra 0.1.0 → 0.3.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.
@@ -0,0 +1,271 @@
1
+ import {afterEach, beforeEach, describe, expect, it} from 'bun:test'
2
+
3
+ import {
4
+ checkHttpReachability,
5
+ checkUsageStats,
6
+ checkVersion,
7
+ formatDurationMs,
8
+ levelLabel,
9
+ stripTrailingSlash,
10
+ toNumber,
11
+ } from './cliproxy-status'
12
+
13
+ const originalFetch = globalThis.fetch
14
+
15
+ type FetchReplacement = (url: string, init?: RequestInit) => Promise<Response>
16
+
17
+ function createFetchImplementation(handler: FetchReplacement): typeof fetch {
18
+ return Object.assign(
19
+ (input: string | URL | Request, init?: RequestInit) => {
20
+ if (typeof input !== 'string') {
21
+ throw new TypeError(`Unexpected non-string fetch input: ${String(input)}`)
22
+ }
23
+
24
+ return handler(input, init)
25
+ },
26
+ {preconnect: originalFetch.preconnect},
27
+ )
28
+ }
29
+
30
+ describe('cliproxy status helpers', () => {
31
+ beforeEach(() => {
32
+ globalThis.fetch = createFetchImplementation(async () => {
33
+ throw new Error('Unexpected fetch call')
34
+ })
35
+ })
36
+
37
+ afterEach(() => {
38
+ globalThis.fetch = originalFetch
39
+ })
40
+
41
+ describe('levelLabel', () => {
42
+ it('returns OK for ok', () => {
43
+ expect(levelLabel('ok')).toBe('OK')
44
+ })
45
+
46
+ it('returns WARN for warning', () => {
47
+ expect(levelLabel('warning')).toBe('WARN')
48
+ })
49
+
50
+ it('returns ERROR for error', () => {
51
+ expect(levelLabel('error')).toBe('ERROR')
52
+ })
53
+ })
54
+
55
+ describe('formatDurationMs', () => {
56
+ it('formats positive durations', () => {
57
+ expect(formatDurationMs(150)).toBe('150ms')
58
+ })
59
+
60
+ it('formats zero duration', () => {
61
+ expect(formatDurationMs(0)).toBe('0ms')
62
+ })
63
+
64
+ it('clamps negative durations to zero', () => {
65
+ expect(formatDurationMs(-5)).toBe('0ms')
66
+ })
67
+ })
68
+
69
+ describe('stripTrailingSlash', () => {
70
+ it('removes a trailing slash', () => {
71
+ expect(stripTrailingSlash('https://example.com/')).toBe('https://example.com')
72
+ })
73
+
74
+ it('does nothing when there is no trailing slash', () => {
75
+ expect(stripTrailingSlash('https://example.com')).toBe('https://example.com')
76
+ })
77
+ })
78
+
79
+ describe('toNumber', () => {
80
+ it('returns finite numbers as-is', () => {
81
+ expect(toNumber(42)).toBe(42)
82
+ })
83
+
84
+ it('returns null for non-numbers', () => {
85
+ expect(toNumber('not a number')).toBeNull()
86
+ })
87
+
88
+ it('returns null for NaN', () => {
89
+ expect(toNumber(Number.NaN)).toBeNull()
90
+ })
91
+
92
+ it('returns null for infinity', () => {
93
+ expect(toNumber(Number.POSITIVE_INFINITY)).toBeNull()
94
+ })
95
+ })
96
+
97
+ describe('checkHttpReachability', () => {
98
+ it('returns ok for HTTP 200', async () => {
99
+ globalThis.fetch = createFetchImplementation(async () => new Response('ok', {status: 200}))
100
+
101
+ const result = await checkHttpReachability('https://cliproxy.example.com', false)
102
+
103
+ expect(result.level).toBe('ok')
104
+ expect(result.summary).toContain('GET https://cliproxy.example.com → 200')
105
+ expect(result.summary).toContain('ms')
106
+ })
107
+
108
+ it('returns error for HTTP 500', async () => {
109
+ globalThis.fetch = createFetchImplementation(async () => new Response('error', {status: 500}))
110
+
111
+ const result = await checkHttpReachability('https://cliproxy.example.com', false)
112
+
113
+ expect(result.level).toBe('error')
114
+ expect(result.summary).toContain('GET https://cliproxy.example.com → 500')
115
+ })
116
+
117
+ it('returns error details for network failures', async () => {
118
+ globalThis.fetch = createFetchImplementation(async () => {
119
+ throw new Error('Network timeout')
120
+ })
121
+
122
+ const result = await checkHttpReachability('https://cliproxy.example.com', true)
123
+
124
+ expect(result.level).toBe('error')
125
+ expect(result.summary).toContain('Network timeout')
126
+ expect(result.details).toEqual(['URL: https://cliproxy.example.com', 'Timeout: 10000ms'])
127
+ })
128
+ })
129
+
130
+ describe('checkUsageStats', () => {
131
+ it('returns ok when failures are zero', async () => {
132
+ globalThis.fetch = createFetchImplementation(
133
+ async () =>
134
+ new Response(JSON.stringify({total_requests: 10, failure_count: 0}), {
135
+ status: 200,
136
+ headers: {'content-type': 'application/json'},
137
+ }),
138
+ )
139
+
140
+ const result = await checkUsageStats('https://cliproxy.example.com', 'secret')
141
+
142
+ expect(result.level).toBe('ok')
143
+ expect(result.summary).toBe('total_requests=10, failure_count=0')
144
+ })
145
+
146
+ it('returns warning when token refresh is likely needed', async () => {
147
+ globalThis.fetch = createFetchImplementation(
148
+ async () =>
149
+ new Response(JSON.stringify({total_requests: 10, failure_count: 3}), {
150
+ status: 200,
151
+ headers: {'content-type': 'application/json'},
152
+ }),
153
+ )
154
+
155
+ const result = await checkUsageStats('https://cliproxy.example.com', 'secret')
156
+
157
+ expect(result.level).toBe('warning')
158
+ expect(result.summary).toContain('total_requests=10, failure_count=3')
159
+ expect(result.summary).toContain('token refresh likely needed')
160
+ })
161
+
162
+ it('returns warning when rate limited', async () => {
163
+ globalThis.fetch = createFetchImplementation(async () => new Response('rate limited', {status: 429}))
164
+
165
+ const result = await checkUsageStats('https://cliproxy.example.com', 'secret')
166
+
167
+ expect(result.level).toBe('warning')
168
+ expect(result.summary).toContain('Rate limited')
169
+ })
170
+
171
+ it('returns error for non-200 responses', async () => {
172
+ globalThis.fetch = createFetchImplementation(async () => new Response('boom', {status: 500}))
173
+
174
+ const result = await checkUsageStats('https://cliproxy.example.com', 'secret')
175
+
176
+ expect(result.level).toBe('error')
177
+ expect(result.summary).toContain('HTTP 500')
178
+ })
179
+
180
+ it('returns error for network failures', async () => {
181
+ globalThis.fetch = createFetchImplementation(async () => {
182
+ throw new Error('socket hang up')
183
+ })
184
+
185
+ const result = await checkUsageStats('https://cliproxy.example.com', 'secret')
186
+
187
+ expect(result.level).toBe('error')
188
+ expect(result.summary).toContain('Unable to read usage stats: socket hang up')
189
+ })
190
+ })
191
+
192
+ describe('checkVersion', () => {
193
+ it('returns ok for JSON string payloads', async () => {
194
+ globalThis.fetch = createFetchImplementation(
195
+ async () =>
196
+ new Response(JSON.stringify('1.2.3'), {
197
+ status: 200,
198
+ headers: {'content-type': 'application/json'},
199
+ }),
200
+ )
201
+
202
+ const result = await checkVersion('https://cliproxy.example.com', 'secret')
203
+
204
+ expect(result.level).toBe('ok')
205
+ expect(result.summary).toBe('1.2.3')
206
+ })
207
+
208
+ it('returns ok for object payloads with version', async () => {
209
+ globalThis.fetch = createFetchImplementation(
210
+ async () =>
211
+ new Response(JSON.stringify({version: '1.2.3'}), {
212
+ status: 200,
213
+ headers: {'content-type': 'application/json'},
214
+ }),
215
+ )
216
+
217
+ const result = await checkVersion('https://cliproxy.example.com', 'secret')
218
+
219
+ expect(result.level).toBe('ok')
220
+ expect(result.summary).toBe('1.2.3')
221
+ })
222
+
223
+ it('returns warning for empty version strings', async () => {
224
+ globalThis.fetch = createFetchImplementation(
225
+ async () =>
226
+ new Response(JSON.stringify({version: ''}), {
227
+ status: 200,
228
+ headers: {'content-type': 'application/json'},
229
+ }),
230
+ )
231
+
232
+ const result = await checkVersion('https://cliproxy.example.com', 'secret')
233
+
234
+ expect(result.level).toBe('warning')
235
+ expect(result.summary).toContain('did not include a usable version string')
236
+ })
237
+
238
+ it('returns warning for missing version fields', async () => {
239
+ globalThis.fetch = createFetchImplementation(
240
+ async () =>
241
+ new Response(JSON.stringify({}), {
242
+ status: 200,
243
+ headers: {'content-type': 'application/json'},
244
+ }),
245
+ )
246
+
247
+ const result = await checkVersion('https://cliproxy.example.com', 'secret')
248
+
249
+ expect(result.level).toBe('warning')
250
+ expect(result.summary).toContain('did not include a usable version string')
251
+ })
252
+
253
+ it('returns warning when rate limited', async () => {
254
+ globalThis.fetch = createFetchImplementation(async () => new Response('rate limited', {status: 429}))
255
+
256
+ const result = await checkVersion('https://cliproxy.example.com', 'secret')
257
+
258
+ expect(result.level).toBe('warning')
259
+ expect(result.summary).toContain('Rate limited')
260
+ })
261
+
262
+ it('returns error for non-200 responses', async () => {
263
+ globalThis.fetch = createFetchImplementation(async () => new Response('boom', {status: 500}))
264
+
265
+ const result = await checkVersion('https://cliproxy.example.com', 'secret')
266
+
267
+ expect(result.level).toBe('error')
268
+ expect(result.summary).toContain('HTTP 500')
269
+ })
270
+ })
271
+ })
@@ -0,0 +1,274 @@
1
+ import type {goke} from 'goke'
2
+
3
+ import {z} from 'zod'
4
+
5
+ const DEFAULT_CLIPROXY_URL = 'https://cliproxy.fro.bot'
6
+ const HTTP_TIMEOUT_MS = 10_000
7
+
8
+ type CheckLevel = 'ok' | 'warning' | 'error'
9
+
10
+ interface CheckResult {
11
+ title: string
12
+ level: CheckLevel
13
+ summary: string
14
+ details?: string[]
15
+ }
16
+
17
+ export function levelLabel(level: CheckLevel): string {
18
+ if (level === 'ok') {
19
+ return 'OK'
20
+ }
21
+
22
+ if (level === 'warning') {
23
+ return 'WARN'
24
+ }
25
+
26
+ return 'ERROR'
27
+ }
28
+
29
+ export function formatDurationMs(durationMs: number): string {
30
+ return `${Math.max(0, Math.round(durationMs))}ms`
31
+ }
32
+
33
+ export function stripTrailingSlash(value: string): string {
34
+ return value.endsWith('/') ? value.slice(0, -1) : value
35
+ }
36
+
37
+ function managementHeaders(key: string): Headers {
38
+ const headers = new Headers()
39
+ headers.set('authorization', `Bearer ${key}`)
40
+ headers.set('x-management-key', key)
41
+ return headers
42
+ }
43
+
44
+ async function parseJsonResponse(response: Response): Promise<unknown> {
45
+ try {
46
+ return await response.json()
47
+ } catch {
48
+ return null
49
+ }
50
+ }
51
+
52
+ export function toNumber(value: unknown): number | null {
53
+ return typeof value === 'number' && Number.isFinite(value) ? value : null
54
+ }
55
+
56
+ export async function checkHttpReachability(url: string, verbose: boolean): Promise<CheckResult> {
57
+ const startedAt = performance.now()
58
+
59
+ try {
60
+ const response = await fetch(url, {
61
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
62
+ })
63
+ const elapsedMs = performance.now() - startedAt
64
+ const details: string[] = []
65
+
66
+ if (verbose) {
67
+ details.push(`URL: ${url}`)
68
+ details.push(`Status text: ${response.statusText || '(none)'}`)
69
+ if (response.headers.get('content-type')) {
70
+ details.push(`Content-Type: ${response.headers.get('content-type')}`)
71
+ }
72
+ }
73
+
74
+ return {
75
+ title: 'HTTP reachability',
76
+ level: response.ok ? 'ok' : 'error',
77
+ summary: `GET ${url} → ${response.status} (${formatDurationMs(elapsedMs)})`,
78
+ details,
79
+ }
80
+ } catch (error) {
81
+ const elapsedMs = performance.now() - startedAt
82
+ const message = error instanceof Error ? error.message : String(error)
83
+
84
+ return {
85
+ title: 'HTTP reachability',
86
+ level: 'error',
87
+ summary: `Request failed after ${formatDurationMs(elapsedMs)}: ${message}`,
88
+ details: verbose ? [`URL: ${url}`, `Timeout: ${HTTP_TIMEOUT_MS}ms`] : undefined,
89
+ }
90
+ }
91
+ }
92
+
93
+ export async function checkUsageStats(baseUrl: string, key: string): Promise<CheckResult> {
94
+ const endpoint = `${baseUrl}/v0/management/usage`
95
+
96
+ try {
97
+ const response = await fetch(endpoint, {
98
+ headers: managementHeaders(key),
99
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
100
+ })
101
+
102
+ if (response.status === 429) {
103
+ return {
104
+ title: 'Usage stats',
105
+ level: 'warning',
106
+ summary: 'Rate limited by management API (HTTP 429). Retry in a few moments.',
107
+ }
108
+ }
109
+
110
+ if (!response.ok) {
111
+ return {
112
+ title: 'Usage stats',
113
+ level: 'error',
114
+ summary: `GET /v0/management/usage failed with HTTP ${response.status}`,
115
+ }
116
+ }
117
+
118
+ const payload = await parseJsonResponse(response)
119
+ const record = payload && typeof payload === 'object' ? payload : {}
120
+ const totalRequests = toNumber((record as Record<string, unknown>).total_requests)
121
+ const failureCount = toNumber((record as Record<string, unknown>).failure_count)
122
+
123
+ if (totalRequests === null || failureCount === null) {
124
+ return {
125
+ title: 'Usage stats',
126
+ level: 'warning',
127
+ summary: 'Management usage payload is missing expected numeric fields.',
128
+ }
129
+ }
130
+
131
+ return {
132
+ title: 'Usage stats',
133
+ level: failureCount > 0 ? 'warning' : 'ok',
134
+ summary:
135
+ failureCount > 0
136
+ ? `total_requests=${totalRequests}, failure_count=${failureCount} (token refresh likely needed)`
137
+ : `total_requests=${totalRequests}, failure_count=${failureCount}`,
138
+ }
139
+ } catch (error) {
140
+ const message = error instanceof Error ? error.message : String(error)
141
+ return {
142
+ title: 'Usage stats',
143
+ level: 'error',
144
+ summary: `Unable to read usage stats: ${message}`,
145
+ }
146
+ }
147
+ }
148
+
149
+ export async function checkVersion(baseUrl: string, key: string): Promise<CheckResult> {
150
+ const endpoint = `${baseUrl}/v0/management/latest-version`
151
+
152
+ try {
153
+ const response = await fetch(endpoint, {
154
+ headers: managementHeaders(key),
155
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
156
+ })
157
+
158
+ if (response.status === 429) {
159
+ return {
160
+ title: 'Current version',
161
+ level: 'warning',
162
+ summary: 'Rate limited by management API (HTTP 429). Retry in a few moments.',
163
+ }
164
+ }
165
+
166
+ if (!response.ok) {
167
+ return {
168
+ title: 'Current version',
169
+ level: 'error',
170
+ summary: `GET /v0/management/latest-version failed with HTTP ${response.status}`,
171
+ }
172
+ }
173
+
174
+ const payload = await parseJsonResponse(response)
175
+
176
+ if (typeof payload === 'string' && payload.length > 0) {
177
+ return {
178
+ title: 'Current version',
179
+ level: 'ok',
180
+ summary: payload,
181
+ }
182
+ }
183
+
184
+ if (payload && typeof payload === 'object') {
185
+ const version = (payload as Record<string, unknown>).version
186
+ if (typeof version === 'string' && version.length > 0) {
187
+ return {
188
+ title: 'Current version',
189
+ level: 'ok',
190
+ summary: version,
191
+ }
192
+ }
193
+ }
194
+
195
+ return {
196
+ title: 'Current version',
197
+ level: 'warning',
198
+ summary: 'Management version payload did not include a usable version string.',
199
+ }
200
+ } catch (error) {
201
+ const message = error instanceof Error ? error.message : String(error)
202
+ return {
203
+ title: 'Current version',
204
+ level: 'error',
205
+ summary: `Unable to read current version: ${message}`,
206
+ }
207
+ }
208
+ }
209
+
210
+ function printCheckResult(result: CheckResult): void {
211
+ console.log(`[${levelLabel(result.level)}] ${result.title}`)
212
+ console.log(` ${result.summary}`)
213
+
214
+ if (result.details && result.details.length > 0) {
215
+ for (const detail of result.details) {
216
+ console.log(` - ${detail}`)
217
+ }
218
+ }
219
+ }
220
+
221
+ export function registerCliproxyStatus(cli: ReturnType<typeof goke>): void {
222
+ cli
223
+ .command('cliproxy status', 'Show operational health of CLIProxyAPI and its management endpoints.')
224
+ .option(
225
+ '--url [url]',
226
+ z
227
+ .string()
228
+ .describe('Base URL for CLIProxyAPI health checks. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.'),
229
+ )
230
+ .option(
231
+ '--key [key]',
232
+ z.string().describe('Management API bearer token. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.'),
233
+ )
234
+ .action(async options => {
235
+ const verbose = options.verbose === true
236
+ const baseUrl = stripTrailingSlash(options.url ?? process.env.CLIPROXY_URL ?? DEFAULT_CLIPROXY_URL)
237
+ const managementKey = options.key ?? process.env.CLIPROXY_MANAGEMENT_KEY
238
+
239
+ console.log('CLIProxyAPI status')
240
+ console.log('')
241
+
242
+ const results: CheckResult[] = [await checkHttpReachability(baseUrl, verbose)]
243
+
244
+ if (managementKey) {
245
+ const [usageResult, versionResult] = await Promise.all([
246
+ checkUsageStats(baseUrl, managementKey),
247
+ checkVersion(baseUrl, managementKey),
248
+ ])
249
+
250
+ results.push(usageResult, versionResult)
251
+ } else {
252
+ results.push({
253
+ title: 'Management checks',
254
+ level: 'warning',
255
+ summary:
256
+ 'CLIPROXY_MANAGEMENT_KEY is not set. Skipping usage stats and version checks. Provide --key or set env var.',
257
+ })
258
+ }
259
+
260
+ for (const result of results) {
261
+ printCheckResult(result)
262
+ console.log('')
263
+ }
264
+
265
+ const errorCount = results.filter(result => result.level === 'error').length
266
+ const warningCount = results.filter(result => result.level === 'warning').length
267
+
268
+ console.log(`Summary: ${results.length} checks, ${errorCount} errors, ${warningCount} warnings`)
269
+
270
+ if (errorCount > 0) {
271
+ process.exitCode = 1
272
+ }
273
+ })
274
+ }