@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,293 +0,0 @@
|
|
|
1
|
-
import type {goke} from 'goke'
|
|
2
|
-
import type {ActionCtx} from '../../lib/action-ctx'
|
|
3
|
-
import type {StatusSummary} from '../status'
|
|
4
|
-
import {z} from 'zod'
|
|
5
|
-
|
|
6
|
-
import {buildKnownHostsArgs} from '../../lib/known-hosts'
|
|
7
|
-
import {redactHost} from '../../lib/redact'
|
|
8
|
-
import {buildIdentityArgs} from '../../lib/ssh-identity'
|
|
9
|
-
import {validateDashboardHost} from './host'
|
|
10
|
-
|
|
11
|
-
declare const process: {
|
|
12
|
-
env: Record<string, string | undefined>
|
|
13
|
-
exitCode?: number
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const COMPOSE_PROJECT_DIR = '/opt/dashboard'
|
|
17
|
-
|
|
18
|
-
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
export interface ComposePsEntry {
|
|
21
|
-
Name: string
|
|
22
|
-
State: string
|
|
23
|
-
Health: string
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export type HealthStatus = 'healthy' | 'unhealthy' | 'starting' | 'n-a'
|
|
27
|
-
|
|
28
|
-
export interface ServiceRow {
|
|
29
|
-
service: string
|
|
30
|
-
state: string
|
|
31
|
-
health: HealthStatus
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface DashboardStatusResult {
|
|
35
|
-
ok: boolean
|
|
36
|
-
services: ServiceRow[]
|
|
37
|
-
error?: string
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export type SpawnFn = (
|
|
41
|
-
cmd: string[],
|
|
42
|
-
opts: {env: Record<string, string>; stdout: 'pipe'; stderr: 'pipe'},
|
|
43
|
-
) => {
|
|
44
|
-
stdout: ReadableStream<Uint8Array>
|
|
45
|
-
stderr: ReadableStream<Uint8Array>
|
|
46
|
-
exited: Promise<number>
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ─── Pure helpers ─────────────────────────────────────────────────────────────
|
|
50
|
-
|
|
51
|
-
export function parseComposePsOutput(stdoutText: string): ComposePsEntry[] {
|
|
52
|
-
const trimmed = stdoutText.trim()
|
|
53
|
-
if (trimmed.length === 0) return []
|
|
54
|
-
|
|
55
|
-
// Legacy compose may emit a single JSON array; current versions emit NDJSON.
|
|
56
|
-
if (trimmed.startsWith('[')) {
|
|
57
|
-
const parsed: unknown = JSON.parse(trimmed)
|
|
58
|
-
return Array.isArray(parsed) ? (parsed as ComposePsEntry[]) : []
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// NDJSON: one JSON object per non-empty line.
|
|
62
|
-
return trimmed
|
|
63
|
-
.split('\n')
|
|
64
|
-
.map(line => line.trim())
|
|
65
|
-
.filter(line => line.length > 0)
|
|
66
|
-
.map(line => JSON.parse(line) as ComposePsEntry)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function normalizeHealth(raw: string): HealthStatus {
|
|
70
|
-
if (raw === 'healthy' || raw === 'unhealthy' || raw === 'starting') {
|
|
71
|
-
return raw
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return 'n-a'
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function parseComposePs(entries: ComposePsEntry[]): ServiceRow[] {
|
|
78
|
-
return entries.map(entry => ({
|
|
79
|
-
service: entry.Name,
|
|
80
|
-
state: entry.State,
|
|
81
|
-
health: normalizeHealth(entry.Health),
|
|
82
|
-
}))
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function isAllRunning(rows: ServiceRow[]): boolean {
|
|
86
|
-
return rows.every(row => row.state === 'running' && (row.health === 'healthy' || row.health === 'n-a'))
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ─── SSH-backed status fetch ──────────────────────────────────────────────────
|
|
90
|
-
|
|
91
|
-
function defaultSpawn(
|
|
92
|
-
cmd: string[],
|
|
93
|
-
opts: {env: Record<string, string>; stdout: 'pipe'; stderr: 'pipe'},
|
|
94
|
-
): ReturnType<SpawnFn> {
|
|
95
|
-
return Bun.spawn(cmd, opts) as ReturnType<SpawnFn>
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export async function getDashboardComposeStatus(
|
|
99
|
-
host: string,
|
|
100
|
-
spawn: SpawnFn = defaultSpawn,
|
|
101
|
-
): Promise<DashboardStatusResult> {
|
|
102
|
-
validateDashboardHost(host)
|
|
103
|
-
|
|
104
|
-
const knownHostsArgs = buildKnownHostsArgs()
|
|
105
|
-
const {args: identityArgs, cleanup} = buildIdentityArgs(process.env.DASHBOARD_SSH_KEY)
|
|
106
|
-
|
|
107
|
-
const sshCmd = [
|
|
108
|
-
'ssh',
|
|
109
|
-
'-o',
|
|
110
|
-
'BatchMode=yes',
|
|
111
|
-
'-o',
|
|
112
|
-
'ConnectTimeout=10',
|
|
113
|
-
'-o',
|
|
114
|
-
'StrictHostKeyChecking=yes',
|
|
115
|
-
...knownHostsArgs,
|
|
116
|
-
...identityArgs,
|
|
117
|
-
`root@${host}`,
|
|
118
|
-
`docker compose --project-directory ${COMPOSE_PROJECT_DIR} ps --format json`,
|
|
119
|
-
]
|
|
120
|
-
|
|
121
|
-
const env: Record<string, string> = {
|
|
122
|
-
PATH: process.env.PATH ?? '/usr/bin:/bin',
|
|
123
|
-
HOME: process.env.HOME ?? '/tmp',
|
|
124
|
-
...(process.env.SSH_AUTH_SOCK ? {SSH_AUTH_SOCK: process.env.SSH_AUTH_SOCK} : {}),
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
let stdoutText: string
|
|
128
|
-
let stderrText: string
|
|
129
|
-
let exitCode: number
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
const child = spawn(sshCmd, {env, stdout: 'pipe', stderr: 'pipe'})
|
|
133
|
-
;[stdoutText, stderrText, exitCode] = await Promise.all([
|
|
134
|
-
new Response(child.stdout).text(),
|
|
135
|
-
new Response(child.stderr).text(),
|
|
136
|
-
child.exited,
|
|
137
|
-
])
|
|
138
|
-
} finally {
|
|
139
|
-
cleanup()
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (exitCode !== 0) {
|
|
143
|
-
return {
|
|
144
|
-
ok: false,
|
|
145
|
-
services: [],
|
|
146
|
-
error: `SSH command failed (exit ${exitCode}): ${redactHost(stderrText.trim(), host) || 'unknown error'}`,
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
let entries: ComposePsEntry[]
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
entries = parseComposePsOutput(stdoutText)
|
|
154
|
-
} catch {
|
|
155
|
-
return {
|
|
156
|
-
ok: false,
|
|
157
|
-
services: [],
|
|
158
|
-
error: `Failed to parse docker compose ps output: ${redactHost(stdoutText.slice(0, 200), host)}`,
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const services = parseComposePs(entries)
|
|
163
|
-
const ok = isAllRunning(services)
|
|
164
|
-
|
|
165
|
-
return {ok, services}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ─── Unified status aggregator export ────────────────────────────────────────
|
|
169
|
-
|
|
170
|
-
export async function getDashboardStatusSummary(host: string, spawn?: SpawnFn): Promise<StatusSummary> {
|
|
171
|
-
let result: DashboardStatusResult
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
result = await getDashboardComposeStatus(host, spawn)
|
|
175
|
-
} catch (error) {
|
|
176
|
-
const errorMsg = error instanceof Error ? error.message : String(error)
|
|
177
|
-
return {
|
|
178
|
-
app: 'dashboard',
|
|
179
|
-
http: `ERROR: ${errorMsg}`,
|
|
180
|
-
lastDeploy: '—',
|
|
181
|
-
version: '—',
|
|
182
|
-
contentHash: '—',
|
|
183
|
-
usageStats: '—',
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Three-way split: no services → ERROR, services present but degraded → DEGRADED, all healthy → OK
|
|
188
|
-
if (result.services.length === 0) {
|
|
189
|
-
const errorMsg = result.error ?? 'No services reported'
|
|
190
|
-
return {
|
|
191
|
-
app: 'dashboard',
|
|
192
|
-
http: `ERROR: ${errorMsg}`,
|
|
193
|
-
lastDeploy: '—',
|
|
194
|
-
version: '—',
|
|
195
|
-
contentHash: '—',
|
|
196
|
-
usageStats: '—',
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const rows = result.services.map(s => `${s.service}:${s.state}/${s.health}`).join(', ')
|
|
201
|
-
|
|
202
|
-
if (!result.ok) {
|
|
203
|
-
return {
|
|
204
|
-
app: 'dashboard',
|
|
205
|
-
http: `DEGRADED: ${rows}`,
|
|
206
|
-
lastDeploy: '—',
|
|
207
|
-
version: '—',
|
|
208
|
-
contentHash: '—',
|
|
209
|
-
usageStats: '—',
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
app: 'dashboard',
|
|
215
|
-
http: `OK: ${rows}`,
|
|
216
|
-
lastDeploy: '—',
|
|
217
|
-
version: '—',
|
|
218
|
-
contentHash: '—',
|
|
219
|
-
usageStats: '—',
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ─── Action (exported for direct testing) ────────────────────────────────────
|
|
224
|
-
|
|
225
|
-
export async function dashboardStatusAction(
|
|
226
|
-
options: {key?: string | undefined},
|
|
227
|
-
ctx: ActionCtx,
|
|
228
|
-
spawn?: SpawnFn,
|
|
229
|
-
): Promise<void> {
|
|
230
|
-
const hostEnvKey = options.key ?? 'DASHBOARD_DOMAIN'
|
|
231
|
-
const host = process.env[hostEnvKey]
|
|
232
|
-
|
|
233
|
-
if (!host) {
|
|
234
|
-
ctx.console.error(
|
|
235
|
-
'Dashboard host not set. Export DASHBOARD_DOMAIN or pass --key <env-name> pointing to a set variable.',
|
|
236
|
-
)
|
|
237
|
-
ctx.process.exit(1)
|
|
238
|
-
return
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
ctx.console.log('Dashboard status')
|
|
242
|
-
ctx.console.log('')
|
|
243
|
-
|
|
244
|
-
let result: DashboardStatusResult
|
|
245
|
-
|
|
246
|
-
try {
|
|
247
|
-
result = await getDashboardComposeStatus(host, spawn)
|
|
248
|
-
} catch (error) {
|
|
249
|
-
ctx.console.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
|
|
250
|
-
ctx.process.exit(1)
|
|
251
|
-
return
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (result.services.length === 0) {
|
|
255
|
-
const msg = result.error ?? 'No services reported by docker compose ps'
|
|
256
|
-
ctx.console.error(`Error: ${msg}`)
|
|
257
|
-
ctx.process.exit(1)
|
|
258
|
-
return
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
ctx.console.log('Service State Health')
|
|
262
|
-
ctx.console.log('─────────────────────────────────────')
|
|
263
|
-
|
|
264
|
-
for (const row of result.services) {
|
|
265
|
-
const svc = row.service.padEnd(16)
|
|
266
|
-
const state = row.state.padEnd(10)
|
|
267
|
-
ctx.console.log(`${svc} ${state} ${row.health}`)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
ctx.console.log('')
|
|
271
|
-
|
|
272
|
-
if (result.ok) {
|
|
273
|
-
ctx.console.log('Status: OK')
|
|
274
|
-
} else {
|
|
275
|
-
ctx.console.log('Status: DEGRADED (one or more services not running)')
|
|
276
|
-
ctx.process.exit(1)
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// ─── Command registration ─────────────────────────────────────────────────────
|
|
281
|
-
|
|
282
|
-
export function registerDashboardStatus(cli: ReturnType<typeof goke>): void {
|
|
283
|
-
cli
|
|
284
|
-
.command('dashboard status', 'Show operational health of the dashboard deployment via docker compose ps.')
|
|
285
|
-
.option(
|
|
286
|
-
'--key [key]',
|
|
287
|
-
z
|
|
288
|
-
.string()
|
|
289
|
-
.optional()
|
|
290
|
-
.describe('Environment variable name holding the SSH host. Falls back to DASHBOARD_DOMAIN when omitted.'),
|
|
291
|
-
)
|
|
292
|
-
.action((options, ctx) => dashboardStatusAction(options, ctx))
|
|
293
|
-
}
|