@marcusrbrown/infra 0.13.1 → 0.13.3
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 +11965 -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,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
|
-
}
|