@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.
- package/dist/cli.js +4977 -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,222 +0,0 @@
|
|
|
1
|
-
import type {StatusSummary} from './status'
|
|
2
|
-
import {describe, expect, it} from 'bun:test'
|
|
3
|
-
import {goke} from 'goke'
|
|
4
|
-
import {createCapturedCtx, expectCapturedToInclude} from '../__test__/mcp-ctx-fixture'
|
|
5
|
-
import {registerStatus, unifiedStatusAction} from './status'
|
|
6
|
-
|
|
7
|
-
// NOTE: dashboard is included in the unified status table alongside the other apps.
|
|
8
|
-
|
|
9
|
-
const healthyKeeweb: StatusSummary = {
|
|
10
|
-
app: 'keeweb',
|
|
11
|
-
http: 'OK',
|
|
12
|
-
lastDeploy: '2026-04-12 10:00',
|
|
13
|
-
version: '—',
|
|
14
|
-
contentHash: 'match',
|
|
15
|
-
usageStats: '—',
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const healthyCliproxy: StatusSummary = {
|
|
19
|
-
app: 'cliproxy',
|
|
20
|
-
http: 'OK',
|
|
21
|
-
lastDeploy: '—',
|
|
22
|
-
version: 'v1.2.3',
|
|
23
|
-
contentHash: '—',
|
|
24
|
-
usageStats: '12 req / 0 fail',
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const healthyGateway: StatusSummary = {
|
|
28
|
-
app: 'gateway',
|
|
29
|
-
http: 'OK: gateway:running/healthy',
|
|
30
|
-
lastDeploy: '—',
|
|
31
|
-
version: '—',
|
|
32
|
-
contentHash: '—',
|
|
33
|
-
usageStats: '—',
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const healthyUmami: StatusSummary = {
|
|
37
|
-
app: 'umami',
|
|
38
|
-
http: 'OK: umami:running/healthy',
|
|
39
|
-
lastDeploy: '—',
|
|
40
|
-
version: '—',
|
|
41
|
-
contentHash: '—',
|
|
42
|
-
usageStats: '—',
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const healthyVpn: StatusSummary = {
|
|
46
|
-
app: 'vpn',
|
|
47
|
-
http: 'OK: wg0 up, pubkey:TESTKEY== 1 peer(s)',
|
|
48
|
-
lastDeploy: '—',
|
|
49
|
-
version: '—',
|
|
50
|
-
contentHash: '—',
|
|
51
|
-
usageStats: '—',
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const healthyDashboard: StatusSummary = {
|
|
55
|
-
app: 'dashboard',
|
|
56
|
-
http: 'OK: dashboard:running/healthy',
|
|
57
|
-
lastDeploy: '—',
|
|
58
|
-
version: '—',
|
|
59
|
-
contentHash: '—',
|
|
60
|
-
usageStats: '—',
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function makeDeps(overrides?: Partial<Parameters<typeof registerStatus>[1]>): Parameters<typeof registerStatus>[1] {
|
|
64
|
-
return {
|
|
65
|
-
getKeewebStatusSummary: async () => healthyKeeweb,
|
|
66
|
-
getCliproxyStatusSummary: async () => healthyCliproxy,
|
|
67
|
-
getGatewayStatusSummary: async () => healthyGateway,
|
|
68
|
-
getUmamiStatusSummary: async () => healthyUmami,
|
|
69
|
-
getDashboardStatusSummary: async () => healthyDashboard,
|
|
70
|
-
getVpnStatusSummary: async () => healthyVpn,
|
|
71
|
-
...overrides,
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
describe('top-level status command (Tier-2 ctx capture)', () => {
|
|
76
|
-
it('prints a unified table when all apps are healthy', async () => {
|
|
77
|
-
const {ctx, captured} = createCapturedCtx()
|
|
78
|
-
|
|
79
|
-
await unifiedStatusAction({}, ctx, makeDeps())
|
|
80
|
-
|
|
81
|
-
expect(
|
|
82
|
-
expectCapturedToInclude(captured, '| App | HTTP | Last Deploy | Version | Content Hash | Usage Stats |'),
|
|
83
|
-
).toBe(true)
|
|
84
|
-
expect(expectCapturedToInclude(captured, '| keeweb | OK | 2026-04-12 10:00 | — | match | — |')).toBe(true)
|
|
85
|
-
expect(expectCapturedToInclude(captured, '| cliproxy | OK | — | v1.2.3 | — | 12 req / 0 fail |')).toBe(true)
|
|
86
|
-
expect(expectCapturedToInclude(captured, '| gateway | OK: gateway:running/healthy | — | — | — | — |')).toBe(true)
|
|
87
|
-
expect(expectCapturedToInclude(captured, '| umami | OK: umami:running/healthy | — | — | — | — |')).toBe(true)
|
|
88
|
-
expect(expectCapturedToInclude(captured, '| dashboard | OK: dashboard:running/healthy | — | — | — | — |')).toBe(
|
|
89
|
-
true,
|
|
90
|
-
)
|
|
91
|
-
expect(expectCapturedToInclude(captured, '| vpn | OK: wg0 up, pubkey:TESTKEY== 1 peer(s) | — | — | — | — |')).toBe(
|
|
92
|
-
true,
|
|
93
|
-
)
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('includes a vpn row sourced from getVpnStatusSummary()', async () => {
|
|
97
|
-
const {ctx, captured} = createCapturedCtx()
|
|
98
|
-
|
|
99
|
-
await unifiedStatusAction({}, ctx, makeDeps())
|
|
100
|
-
|
|
101
|
-
// The vpn row must appear in the table output
|
|
102
|
-
expect(expectCapturedToInclude(captured, '| vpn |')).toBe(true)
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('shows an error row for vpn when getVpnStatusSummary rejects', async () => {
|
|
106
|
-
const {ctx, captured} = createCapturedCtx()
|
|
107
|
-
|
|
108
|
-
await unifiedStatusAction(
|
|
109
|
-
{},
|
|
110
|
-
ctx,
|
|
111
|
-
makeDeps({
|
|
112
|
-
getVpnStatusSummary: async () => {
|
|
113
|
-
throw new Error('VPN_HOST not set')
|
|
114
|
-
},
|
|
115
|
-
}),
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
expect(
|
|
119
|
-
expectCapturedToInclude(
|
|
120
|
-
captured,
|
|
121
|
-
'| vpn | ❌ VPN_HOST not set | ❌ VPN_HOST not set | ❌ VPN_HOST not set | ❌ VPN_HOST not set | ❌ VPN_HOST not set |',
|
|
122
|
-
),
|
|
123
|
-
).toBe(true)
|
|
124
|
-
expect(captured.stderr.join('')).toContain('vpn status check failed: VPN_HOST not set')
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('shows an error row when umami is unreachable and keeps the other results', async () => {
|
|
128
|
-
const {ctx, captured} = createCapturedCtx()
|
|
129
|
-
|
|
130
|
-
await unifiedStatusAction(
|
|
131
|
-
{},
|
|
132
|
-
ctx,
|
|
133
|
-
makeDeps({
|
|
134
|
-
getUmamiStatusSummary: async () => {
|
|
135
|
-
throw new Error('UMAMI_DOMAIN not set')
|
|
136
|
-
},
|
|
137
|
-
}),
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
expect(
|
|
141
|
-
expectCapturedToInclude(
|
|
142
|
-
captured,
|
|
143
|
-
'| umami | ❌ UMAMI_DOMAIN not set | ❌ UMAMI_DOMAIN not set | ❌ UMAMI_DOMAIN not set | ❌ UMAMI_DOMAIN not set | ❌ UMAMI_DOMAIN not set |',
|
|
144
|
-
),
|
|
145
|
-
).toBe(true)
|
|
146
|
-
expect(expectCapturedToInclude(captured, '| gateway | OK: gateway:running/healthy | — | — | — | — |')).toBe(true)
|
|
147
|
-
expect(captured.stderr.join('')).toContain('umami status check failed: UMAMI_DOMAIN not set')
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('shows an error row when one app fails and keeps the other results', async () => {
|
|
151
|
-
const {ctx, captured} = createCapturedCtx()
|
|
152
|
-
|
|
153
|
-
await unifiedStatusAction(
|
|
154
|
-
{},
|
|
155
|
-
ctx,
|
|
156
|
-
makeDeps({
|
|
157
|
-
getKeewebStatusSummary: async () => {
|
|
158
|
-
throw new Error('Keeweb exploded')
|
|
159
|
-
},
|
|
160
|
-
}),
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
expect(
|
|
164
|
-
expectCapturedToInclude(
|
|
165
|
-
captured,
|
|
166
|
-
'| keeweb | ❌ Keeweb exploded | ❌ Keeweb exploded | ❌ Keeweb exploded | ❌ Keeweb exploded | ❌ Keeweb exploded |',
|
|
167
|
-
),
|
|
168
|
-
).toBe(true)
|
|
169
|
-
expect(expectCapturedToInclude(captured, '| cliproxy | OK | — | v1.2.3 | — | 12 req / 0 fail |')).toBe(true)
|
|
170
|
-
// Rejection reason must also appear in stderr so MCP consumers can see it
|
|
171
|
-
expect(captured.stderr.join('')).toContain('keeweb status check failed: Keeweb exploded')
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
it('prints valid json with all app keys when --json is passed', async () => {
|
|
175
|
-
const {ctx, captured} = createCapturedCtx()
|
|
176
|
-
|
|
177
|
-
await unifiedStatusAction({json: true}, ctx, makeDeps())
|
|
178
|
-
|
|
179
|
-
expect(captured.stdout).toHaveLength(1)
|
|
180
|
-
|
|
181
|
-
const [jsonOutput] = captured.stdout
|
|
182
|
-
expect(jsonOutput).toBeDefined()
|
|
183
|
-
|
|
184
|
-
const parsed = JSON.parse(jsonOutput ?? '{}') as {
|
|
185
|
-
keeweb: {http: string}
|
|
186
|
-
cliproxy: {version: string}
|
|
187
|
-
gateway: {http: string}
|
|
188
|
-
umami: {http: string}
|
|
189
|
-
dashboard: {http: string}
|
|
190
|
-
vpn: {http: string}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
expect(parsed.keeweb.http).toBe('OK')
|
|
194
|
-
expect(parsed.cliproxy.version).toBe('v1.2.3')
|
|
195
|
-
expect(parsed.gateway.http).toBe('OK: gateway:running/healthy')
|
|
196
|
-
expect(parsed.umami.http).toBe('OK: umami:running/healthy')
|
|
197
|
-
expect(parsed.dashboard.http).toBe('OK: dashboard:running/healthy')
|
|
198
|
-
expect(parsed.vpn.http).toBe('OK: wg0 up, pubkey:TESTKEY== 1 peer(s)')
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
it('does not write to global console (output is captured via ctx)', async () => {
|
|
202
|
-
const {ctx, captured} = createCapturedCtx()
|
|
203
|
-
|
|
204
|
-
await unifiedStatusAction({}, ctx, makeDeps())
|
|
205
|
-
|
|
206
|
-
// Verify output went to ctx capture, not global console
|
|
207
|
-
expect(captured.stdout.length).toBeGreaterThan(0)
|
|
208
|
-
expect(expectCapturedToInclude(captured, '| App |')).toBe(true)
|
|
209
|
-
})
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
describe('top-level status command (registerStatus wiring)', () => {
|
|
213
|
-
it('wires through goke without throwing', async () => {
|
|
214
|
-
const cli = goke('test')
|
|
215
|
-
|
|
216
|
-
registerStatus(cli, makeDeps())
|
|
217
|
-
cli.parse(['bun', 'test', 'status'], {run: false})
|
|
218
|
-
|
|
219
|
-
// Verify the command runs without throwing (goke uses its own ctx for output)
|
|
220
|
-
await expect(cli.runMatchedCommand()).resolves.toBeUndefined()
|
|
221
|
-
})
|
|
222
|
-
})
|
package/src/commands/status.ts
DELETED
|
@@ -1,161 +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 {getCliproxyStatusSummary} from './cliproxy/status'
|
|
8
|
-
import {getDashboardStatusSummary} from './dashboard'
|
|
9
|
-
import {getGatewayStatusSummary} from './gateway'
|
|
10
|
-
import {getKeewebStatusSummary} from './keeweb/status'
|
|
11
|
-
import {getUmamiStatusSummary} from './umami'
|
|
12
|
-
import {getVpnStatusSummary} from './vpn'
|
|
13
|
-
|
|
14
|
-
declare const process: {
|
|
15
|
-
env: Record<string, string | undefined>
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface StatusSummary {
|
|
19
|
-
app: 'keeweb' | 'cliproxy' | 'gateway' | 'umami' | 'dashboard' | 'vpn'
|
|
20
|
-
http: string
|
|
21
|
-
lastDeploy: string
|
|
22
|
-
version: string
|
|
23
|
-
contentHash: string
|
|
24
|
-
usageStats: string
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
type AppName = StatusSummary['app']
|
|
28
|
-
|
|
29
|
-
interface StatusDependencies {
|
|
30
|
-
getKeewebStatusSummary: (verbose: boolean) => Promise<StatusSummary>
|
|
31
|
-
getCliproxyStatusSummary: (baseUrl: string, key: string, verbose: boolean) => Promise<StatusSummary>
|
|
32
|
-
getGatewayStatusSummary: (host: string) => Promise<StatusSummary>
|
|
33
|
-
getUmamiStatusSummary: (host: string) => Promise<StatusSummary>
|
|
34
|
-
getDashboardStatusSummary: (host: string) => Promise<StatusSummary>
|
|
35
|
-
getVpnStatusSummary: (host: string) => Promise<StatusSummary>
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const DEFAULT_CLIPROXY_URL = 'https://cliproxy.fro.bot'
|
|
39
|
-
const ERROR_PREFIX = '❌'
|
|
40
|
-
const TABLE_COLUMNS: {key: keyof StatusSummary; label: string}[] = [
|
|
41
|
-
{key: 'app', label: 'App'},
|
|
42
|
-
{key: 'http', label: 'HTTP'},
|
|
43
|
-
{key: 'lastDeploy', label: 'Last Deploy'},
|
|
44
|
-
{key: 'version', label: 'Version'},
|
|
45
|
-
{key: 'contentHash', label: 'Content Hash'},
|
|
46
|
-
{key: 'usageStats', label: 'Usage Stats'},
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
function stripTrailingSlash(value: string): string {
|
|
50
|
-
return value.endsWith('/') ? value.slice(0, -1) : value
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function errorSummary(app: AppName, reason: unknown): StatusSummary {
|
|
54
|
-
const message = reason instanceof Error ? reason.message : String(reason)
|
|
55
|
-
const errorCell = `${ERROR_PREFIX} ${message}`
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
app,
|
|
59
|
-
http: errorCell,
|
|
60
|
-
lastDeploy: errorCell,
|
|
61
|
-
version: errorCell,
|
|
62
|
-
contentHash: errorCell,
|
|
63
|
-
usageStats: errorCell,
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function toJsonPayload(rows: StatusSummary[]): Record<AppName, StatusSummary> {
|
|
68
|
-
return {
|
|
69
|
-
keeweb: rows.find(row => row.app === 'keeweb') ?? errorSummary('keeweb', 'missing result'),
|
|
70
|
-
cliproxy: rows.find(row => row.app === 'cliproxy') ?? errorSummary('cliproxy', 'missing result'),
|
|
71
|
-
gateway: rows.find(row => row.app === 'gateway') ?? errorSummary('gateway', 'missing result'),
|
|
72
|
-
umami: rows.find(row => row.app === 'umami') ?? errorSummary('umami', 'missing result'),
|
|
73
|
-
dashboard: rows.find(row => row.app === 'dashboard') ?? errorSummary('dashboard', 'missing result'),
|
|
74
|
-
vpn: rows.find(row => row.app === 'vpn') ?? errorSummary('vpn', 'missing result'),
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function formatRow(row: StatusSummary): string {
|
|
79
|
-
const values = TABLE_COLUMNS.map(({key}) => row[key])
|
|
80
|
-
return `| ${values.join(' | ')} |`
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export async function unifiedStatusAction(
|
|
84
|
-
options: {json?: boolean; verbose?: boolean},
|
|
85
|
-
ctx: ActionCtx,
|
|
86
|
-
dependencies: StatusDependencies = {
|
|
87
|
-
getKeewebStatusSummary,
|
|
88
|
-
getCliproxyStatusSummary,
|
|
89
|
-
getGatewayStatusSummary,
|
|
90
|
-
getUmamiStatusSummary,
|
|
91
|
-
getDashboardStatusSummary,
|
|
92
|
-
getVpnStatusSummary,
|
|
93
|
-
},
|
|
94
|
-
): Promise<void> {
|
|
95
|
-
const verbose = options.verbose === true
|
|
96
|
-
const cliproxyBaseUrl = stripTrailingSlash(process.env.CLIPROXY_URL ?? DEFAULT_CLIPROXY_URL)
|
|
97
|
-
const cliproxyKey = process.env.CLIPROXY_MANAGEMENT_KEY ?? ''
|
|
98
|
-
const gatewayHost = process.env.GATEWAY_HOST ?? ''
|
|
99
|
-
const umamiHost = process.env.UMAMI_DOMAIN ?? ''
|
|
100
|
-
const dashboardHost = process.env.DASHBOARD_DOMAIN ?? ''
|
|
101
|
-
const vpnHost = process.env.VPN_HOST ?? ''
|
|
102
|
-
|
|
103
|
-
const results = await Promise.allSettled([
|
|
104
|
-
dependencies.getKeewebStatusSummary(verbose),
|
|
105
|
-
dependencies.getCliproxyStatusSummary(cliproxyBaseUrl, cliproxyKey, verbose),
|
|
106
|
-
dependencies.getGatewayStatusSummary(gatewayHost),
|
|
107
|
-
dependencies.getUmamiStatusSummary(umamiHost),
|
|
108
|
-
dependencies.getDashboardStatusSummary(dashboardHost),
|
|
109
|
-
dependencies.getVpnStatusSummary(vpnHost),
|
|
110
|
-
])
|
|
111
|
-
|
|
112
|
-
const appNames: AppName[] = ['keeweb', 'cliproxy', 'gateway', 'umami', 'dashboard', 'vpn']
|
|
113
|
-
const rows: StatusSummary[] = results.map((result, index) => {
|
|
114
|
-
const app = appNames[index] ?? 'keeweb'
|
|
115
|
-
if (result.status === 'fulfilled') {
|
|
116
|
-
return result.value
|
|
117
|
-
}
|
|
118
|
-
const reason = result.reason
|
|
119
|
-
const message = reason instanceof Error ? reason.message : String(reason)
|
|
120
|
-
ctx.console.error(`${app} status check failed: ${message}`)
|
|
121
|
-
return errorSummary(app, reason)
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
if (options.json === true) {
|
|
125
|
-
ctx.console.log(JSON.stringify(toJsonPayload(rows)))
|
|
126
|
-
return
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
ctx.console.log('| App | HTTP | Last Deploy | Version | Content Hash | Usage Stats |')
|
|
130
|
-
for (const row of rows) {
|
|
131
|
-
ctx.console.log(formatRow(row))
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function registerStatus(
|
|
136
|
-
cli: ReturnType<typeof goke>,
|
|
137
|
-
dependencies: StatusDependencies = {
|
|
138
|
-
getKeewebStatusSummary,
|
|
139
|
-
getCliproxyStatusSummary,
|
|
140
|
-
getGatewayStatusSummary,
|
|
141
|
-
getUmamiStatusSummary,
|
|
142
|
-
getDashboardStatusSummary,
|
|
143
|
-
getVpnStatusSummary,
|
|
144
|
-
},
|
|
145
|
-
): void {
|
|
146
|
-
cli
|
|
147
|
-
.command('status', 'Show status of all deployments')
|
|
148
|
-
.option(
|
|
149
|
-
'--json',
|
|
150
|
-
z
|
|
151
|
-
.boolean()
|
|
152
|
-
.describe(
|
|
153
|
-
'Output machine-readable JSON with keeweb, cliproxy, gateway, umami, dashboard, and vpn summary objects.',
|
|
154
|
-
),
|
|
155
|
-
)
|
|
156
|
-
.option(
|
|
157
|
-
'--verbose',
|
|
158
|
-
z.boolean().describe('Include verbose per-app health check details when building the summary rows.'),
|
|
159
|
-
)
|
|
160
|
-
.action((options, ctx) => unifiedStatusAction(options, ctx, dependencies))
|
|
161
|
-
}
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
import {resolve} from 'node:path'
|
|
2
|
-
import {afterEach, beforeEach, describe, expect, it} from 'bun:test'
|
|
3
|
-
|
|
4
|
-
import {getUmamiDeployEnv, validateUmamiRemotePreconditions} from './deploy'
|
|
5
|
-
|
|
6
|
-
const repoRoot = resolve(import.meta.dir, '../../../../..')
|
|
7
|
-
|
|
8
|
-
const envKeys = [
|
|
9
|
-
'HOME',
|
|
10
|
-
'PATH',
|
|
11
|
-
'SSH_AUTH_SOCK',
|
|
12
|
-
'UMAMI_DOMAIN',
|
|
13
|
-
'UMAMI_APP_SECRET',
|
|
14
|
-
'UMAMI_DB_PASSWORD',
|
|
15
|
-
'UMAMI_ADMIN_PASSWORD',
|
|
16
|
-
'UMAMI_SSH_KEY',
|
|
17
|
-
] as const
|
|
18
|
-
|
|
19
|
-
type ManagedEnvKey = (typeof envKeys)[number]
|
|
20
|
-
|
|
21
|
-
let originalEnv: Partial<Record<ManagedEnvKey, string | undefined>>
|
|
22
|
-
|
|
23
|
-
function restoreManagedEnv(): void {
|
|
24
|
-
for (const key of envKeys) {
|
|
25
|
-
const value = originalEnv[key]
|
|
26
|
-
if (value === undefined) {
|
|
27
|
-
delete process.env[key]
|
|
28
|
-
} else {
|
|
29
|
-
process.env[key] = value
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function setManagedEnv(overrides: Partial<Record<ManagedEnvKey, string | undefined>>): void {
|
|
35
|
-
restoreManagedEnv()
|
|
36
|
-
for (const [key, value] of Object.entries(overrides)) {
|
|
37
|
-
if (value === undefined) {
|
|
38
|
-
delete process.env[key as ManagedEnvKey]
|
|
39
|
-
} else {
|
|
40
|
-
process.env[key as ManagedEnvKey] = value
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
beforeEach(() => {
|
|
46
|
-
originalEnv = {}
|
|
47
|
-
for (const key of envKeys) {
|
|
48
|
-
originalEnv[key] = process.env[key]
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
afterEach(() => {
|
|
53
|
-
restoreManagedEnv()
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
// ─── getUmamiDeployEnv ────────────────────────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
describe('getUmamiDeployEnv', () => {
|
|
59
|
-
it('returns env object with required keys when all are set', () => {
|
|
60
|
-
setManagedEnv({
|
|
61
|
-
PATH: '/usr/bin:/bin',
|
|
62
|
-
HOME: '/home/user',
|
|
63
|
-
SSH_AUTH_SOCK: '/tmp/ssh-agent.sock',
|
|
64
|
-
UMAMI_DOMAIN: 'metrics.fro.bot',
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
const env = getUmamiDeployEnv()
|
|
68
|
-
|
|
69
|
-
expect(env.PATH).toBe('/usr/bin:/bin')
|
|
70
|
-
expect(env.HOME).toBe('/home/user')
|
|
71
|
-
expect(env.SSH_AUTH_SOCK).toBe('/tmp/ssh-agent.sock')
|
|
72
|
-
expect(env.UMAMI_DOMAIN).toBe('metrics.fro.bot')
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('throws when PATH is missing', () => {
|
|
76
|
-
setManagedEnv({PATH: undefined, HOME: '/home/user', SSH_AUTH_SOCK: '/tmp/ssh-agent.sock'})
|
|
77
|
-
|
|
78
|
-
expect(() => getUmamiDeployEnv()).toThrow('PATH is required')
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('throws when HOME is missing', () => {
|
|
82
|
-
setManagedEnv({PATH: '/usr/bin:/bin', HOME: undefined, SSH_AUTH_SOCK: '/tmp/ssh-agent.sock'})
|
|
83
|
-
|
|
84
|
-
expect(() => getUmamiDeployEnv()).toThrow('HOME is required')
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('throws when SSH_AUTH_SOCK is missing and no UMAMI_SSH_KEY either', () => {
|
|
88
|
-
setManagedEnv({
|
|
89
|
-
PATH: '/usr/bin:/bin',
|
|
90
|
-
HOME: '/home/user',
|
|
91
|
-
SSH_AUTH_SOCK: undefined,
|
|
92
|
-
UMAMI_SSH_KEY: undefined,
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
expect(() => getUmamiDeployEnv()).toThrow('Local deploy needs an SSH context')
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('succeeds with only SSH_AUTH_SOCK set (no UMAMI_SSH_KEY)', () => {
|
|
99
|
-
setManagedEnv({
|
|
100
|
-
PATH: '/usr/bin:/bin',
|
|
101
|
-
HOME: '/home/user',
|
|
102
|
-
SSH_AUTH_SOCK: '/tmp/ssh-agent.sock',
|
|
103
|
-
UMAMI_SSH_KEY: undefined,
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
const env = getUmamiDeployEnv()
|
|
107
|
-
|
|
108
|
-
expect(env.SSH_AUTH_SOCK).toBe('/tmp/ssh-agent.sock')
|
|
109
|
-
expect('UMAMI_SSH_KEY' in env).toBe(false)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('succeeds with only UMAMI_SSH_KEY set (no SSH_AUTH_SOCK) and includes the key in env', () => {
|
|
113
|
-
setManagedEnv({
|
|
114
|
-
PATH: '/usr/bin:/bin',
|
|
115
|
-
HOME: '/home/user',
|
|
116
|
-
SSH_AUTH_SOCK: undefined,
|
|
117
|
-
UMAMI_SSH_KEY: 'ssh-ed25519 AAAA...',
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
const env = getUmamiDeployEnv()
|
|
121
|
-
|
|
122
|
-
expect(env.UMAMI_SSH_KEY).toBe('ssh-ed25519 AAAA...')
|
|
123
|
-
expect('SSH_AUTH_SOCK' in env).toBe(false)
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
it('includes optional umami env vars when set', () => {
|
|
127
|
-
setManagedEnv({
|
|
128
|
-
PATH: '/usr/bin:/bin',
|
|
129
|
-
HOME: '/home/user',
|
|
130
|
-
SSH_AUTH_SOCK: '/tmp/ssh-agent.sock',
|
|
131
|
-
UMAMI_APP_SECRET: 'secret123',
|
|
132
|
-
UMAMI_DB_PASSWORD: 'dbpass',
|
|
133
|
-
UMAMI_ADMIN_PASSWORD: 'adminpass',
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
const env = getUmamiDeployEnv()
|
|
137
|
-
|
|
138
|
-
expect(env.UMAMI_APP_SECRET).toBe('secret123')
|
|
139
|
-
expect(env.UMAMI_DB_PASSWORD).toBe('dbpass')
|
|
140
|
-
expect(env.UMAMI_ADMIN_PASSWORD).toBe('adminpass')
|
|
141
|
-
})
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
// ─── validateUmamiRemotePreconditions ─────────────────────────────────────────
|
|
145
|
-
|
|
146
|
-
describe('validateUmamiRemotePreconditions', () => {
|
|
147
|
-
it('throws a clear error when gh is not available', () => {
|
|
148
|
-
// We cannot reliably mock Bun.which, so we test the function contract:
|
|
149
|
-
// if gh is not installed, it should throw with a helpful message.
|
|
150
|
-
// This test verifies the error message shape by calling with a known-missing binary.
|
|
151
|
-
// In CI where gh IS installed, we skip this test.
|
|
152
|
-
if (Bun.which('gh')) {
|
|
153
|
-
// gh is available — just verify the function does not throw
|
|
154
|
-
expect(() => validateUmamiRemotePreconditions()).not.toThrow()
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
expect(() => validateUmamiRemotePreconditions()).toThrow('gh CLI is required')
|
|
159
|
-
})
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
// ─── deploy command (subprocess integration via CLI) ─────────────────────────
|
|
163
|
-
|
|
164
|
-
describe('deploy command', () => {
|
|
165
|
-
it('dry-run remote mode prints planned gh workflow run command without executing', async () => {
|
|
166
|
-
const proc = Bun.spawn(['bun', 'run', 'packages/cli/src/cli.ts', 'umami', 'deploy', '--dry-run'], {
|
|
167
|
-
cwd: repoRoot,
|
|
168
|
-
env: {...process.env, NO_COLOR: '1'},
|
|
169
|
-
stdout: 'pipe',
|
|
170
|
-
stderr: 'pipe',
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
const [stdout, _stderr, exitCode] = await Promise.all([
|
|
174
|
-
new Response(proc.stdout).text(),
|
|
175
|
-
new Response(proc.stderr).text(),
|
|
176
|
-
proc.exited,
|
|
177
|
-
])
|
|
178
|
-
|
|
179
|
-
expect(exitCode).toBe(0)
|
|
180
|
-
expect(stdout).toContain('Dry run')
|
|
181
|
-
expect(stdout).toContain('Deploy Umami')
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
it('dry-run local mode prints planned bun command without executing', async () => {
|
|
185
|
-
const proc = Bun.spawn(['bun', 'run', 'packages/cli/src/cli.ts', 'umami', 'deploy', '--local', '--dry-run'], {
|
|
186
|
-
cwd: repoRoot,
|
|
187
|
-
env: {...process.env, NO_COLOR: '1'},
|
|
188
|
-
stdout: 'pipe',
|
|
189
|
-
stderr: 'pipe',
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
const [stdout, _stderr, exitCode] = await Promise.all([
|
|
193
|
-
new Response(proc.stdout).text(),
|
|
194
|
-
new Response(proc.stderr).text(),
|
|
195
|
-
proc.exited,
|
|
196
|
-
])
|
|
197
|
-
|
|
198
|
-
expect(exitCode).toBe(0)
|
|
199
|
-
expect(stdout).toContain('Dry run')
|
|
200
|
-
expect(stdout).toContain('apps/umami')
|
|
201
|
-
})
|
|
202
|
-
})
|