@marcusrbrown/infra 0.4.11 → 0.5.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.
- package/package.json +2 -1
- package/src/__test__/mcp-ctx-fixture.test.ts +108 -0
- package/src/__test__/mcp-ctx-fixture.ts +104 -0
- package/src/commands/cliproxy/config.test.ts +261 -91
- package/src/commands/cliproxy/config.ts +84 -41
- package/src/commands/cliproxy/keys.test.ts +326 -0
- package/src/commands/cliproxy/keys.ts +116 -60
- package/src/commands/cliproxy/status.test.ts +80 -0
- package/src/commands/cliproxy/status.ts +74 -54
- package/src/commands/gateway/backup.test.ts +66 -1
- package/src/commands/gateway/backup.ts +39 -24
- package/src/commands/gateway/status.test.ts +130 -1
- package/src/commands/gateway/status.ts +50 -39
- package/src/commands/keeweb/status.test.ts +146 -0
- package/src/commands/keeweb/status.ts +32 -32
- package/src/commands/mcp.test.ts +210 -0
- package/src/commands/mcp.ts +32 -3
- package/src/commands/status.test.ts +95 -111
- package/src/commands/status.ts +46 -28
- package/src/lib/action-ctx.ts +25 -0
|
@@ -1,139 +1,123 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {StatusSummary} from './status'
|
|
2
|
+
import {describe, expect, it} from 'bun:test'
|
|
2
3
|
import {goke} from 'goke'
|
|
4
|
+
import {createCapturedCtx, expectCapturedToInclude} from '../__test__/mcp-ctx-fixture'
|
|
5
|
+
import {registerStatus, unifiedStatusAction} from './status'
|
|
6
|
+
|
|
7
|
+
const healthyKeeweb: StatusSummary = {
|
|
8
|
+
app: 'keeweb',
|
|
9
|
+
http: 'OK',
|
|
10
|
+
lastDeploy: '2026-04-12 10:00',
|
|
11
|
+
version: '—',
|
|
12
|
+
contentHash: 'match',
|
|
13
|
+
usageStats: '—',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const healthyCliproxy: StatusSummary = {
|
|
17
|
+
app: 'cliproxy',
|
|
18
|
+
http: 'OK',
|
|
19
|
+
lastDeploy: '—',
|
|
20
|
+
version: 'v1.2.3',
|
|
21
|
+
contentHash: '—',
|
|
22
|
+
usageStats: '12 req / 0 fail',
|
|
23
|
+
}
|
|
3
24
|
|
|
4
|
-
|
|
25
|
+
const healthyGateway: StatusSummary = {
|
|
26
|
+
app: 'gateway',
|
|
27
|
+
http: 'OK: gateway:running/healthy',
|
|
28
|
+
lastDeploy: '—',
|
|
29
|
+
version: '—',
|
|
30
|
+
contentHash: '—',
|
|
31
|
+
usageStats: '—',
|
|
32
|
+
}
|
|
5
33
|
|
|
6
|
-
|
|
7
|
-
|
|
34
|
+
function makeDeps(overrides?: Partial<Parameters<typeof registerStatus>[1]>): Parameters<typeof registerStatus>[1] {
|
|
35
|
+
return {
|
|
36
|
+
getKeewebStatusSummary: async () => healthyKeeweb,
|
|
37
|
+
getCliproxyStatusSummary: async () => healthyCliproxy,
|
|
38
|
+
getGatewayStatusSummary: async () => healthyGateway,
|
|
39
|
+
...overrides,
|
|
40
|
+
}
|
|
8
41
|
}
|
|
9
42
|
|
|
10
|
-
describe('top-level status command', () => {
|
|
11
|
-
|
|
43
|
+
describe('top-level status command (Tier-2 ctx capture)', () => {
|
|
44
|
+
it('prints a unified table when all apps are healthy', async () => {
|
|
45
|
+
const {ctx, captured} = createCapturedCtx()
|
|
12
46
|
|
|
13
|
-
|
|
14
|
-
logSpy = spyOn(console, 'log').mockImplementation(() => undefined)
|
|
15
|
-
process.exitCode = 0
|
|
16
|
-
})
|
|
47
|
+
await unifiedStatusAction({}, ctx, makeDeps())
|
|
17
48
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
49
|
+
expect(
|
|
50
|
+
expectCapturedToInclude(captured, '| App | HTTP | Last Deploy | Version | Content Hash | Usage Stats |'),
|
|
51
|
+
).toBe(true)
|
|
52
|
+
expect(expectCapturedToInclude(captured, '| keeweb | OK | 2026-04-12 10:00 | — | match | — |')).toBe(true)
|
|
53
|
+
expect(expectCapturedToInclude(captured, '| cliproxy | OK | — | v1.2.3 | — | 12 req / 0 fail |')).toBe(true)
|
|
54
|
+
expect(expectCapturedToInclude(captured, '| gateway | OK: gateway:running/healthy | — | — | — | — |')).toBe(true)
|
|
21
55
|
})
|
|
22
56
|
|
|
23
|
-
async
|
|
24
|
-
|
|
25
|
-
dependencies: Parameters<typeof registerStatus>[1],
|
|
26
|
-
): Promise<string[]> {
|
|
27
|
-
const cli = goke('test')
|
|
28
|
-
registerStatus(cli, dependencies)
|
|
29
|
-
cli.parse(['bun', 'test', 'status', ...args], {run: false})
|
|
30
|
-
await cli.runMatchedCommand()
|
|
57
|
+
it('shows an error row when one app fails and keeps the other results', async () => {
|
|
58
|
+
const {ctx, captured} = createCapturedCtx()
|
|
31
59
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
http: 'OK',
|
|
40
|
-
lastDeploy: '2026-04-12 10:00',
|
|
41
|
-
version: '—',
|
|
42
|
-
contentHash: 'match',
|
|
43
|
-
usageStats: '—',
|
|
44
|
-
}),
|
|
45
|
-
getCliproxyStatusSummary: async () => ({
|
|
46
|
-
app: 'cliproxy',
|
|
47
|
-
http: 'OK',
|
|
48
|
-
lastDeploy: '—',
|
|
49
|
-
version: 'v1.2.3',
|
|
50
|
-
contentHash: '—',
|
|
51
|
-
usageStats: '12 req / 0 fail',
|
|
60
|
+
await unifiedStatusAction(
|
|
61
|
+
{},
|
|
62
|
+
ctx,
|
|
63
|
+
makeDeps({
|
|
64
|
+
getKeewebStatusSummary: async () => {
|
|
65
|
+
throw new Error('Keeweb exploded')
|
|
66
|
+
},
|
|
52
67
|
}),
|
|
53
|
-
|
|
54
|
-
app: 'gateway',
|
|
55
|
-
http: 'OK: gateway:running/healthy',
|
|
56
|
-
lastDeploy: '—',
|
|
57
|
-
version: '—',
|
|
58
|
-
contentHash: '—',
|
|
59
|
-
usageStats: '—',
|
|
60
|
-
}),
|
|
61
|
-
})
|
|
68
|
+
)
|
|
62
69
|
|
|
63
|
-
expect(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
expect(
|
|
71
|
+
expectCapturedToInclude(
|
|
72
|
+
captured,
|
|
73
|
+
'| keeweb | ❌ Keeweb exploded | ❌ Keeweb exploded | ❌ Keeweb exploded | ❌ Keeweb exploded | ❌ Keeweb exploded |',
|
|
74
|
+
),
|
|
75
|
+
).toBe(true)
|
|
76
|
+
expect(expectCapturedToInclude(captured, '| cliproxy | OK | — | v1.2.3 | — | 12 req / 0 fail |')).toBe(true)
|
|
77
|
+
// Rejection reason must also appear in stderr so MCP consumers can see it
|
|
78
|
+
expect(captured.stderr.join('')).toContain('keeweb status check failed: Keeweb exploded')
|
|
67
79
|
})
|
|
68
80
|
|
|
69
|
-
it('
|
|
70
|
-
const
|
|
71
|
-
getKeewebStatusSummary: async () => {
|
|
72
|
-
throw new Error('Keeweb exploded')
|
|
73
|
-
},
|
|
74
|
-
getCliproxyStatusSummary: async () => ({
|
|
75
|
-
app: 'cliproxy',
|
|
76
|
-
http: 'OK',
|
|
77
|
-
lastDeploy: '—',
|
|
78
|
-
version: 'v1.2.3',
|
|
79
|
-
contentHash: '—',
|
|
80
|
-
usageStats: '12 req / 0 fail',
|
|
81
|
-
}),
|
|
82
|
-
getGatewayStatusSummary: async () => ({
|
|
83
|
-
app: 'gateway',
|
|
84
|
-
http: 'OK: gateway:running/healthy',
|
|
85
|
-
lastDeploy: '—',
|
|
86
|
-
version: '—',
|
|
87
|
-
contentHash: '—',
|
|
88
|
-
usageStats: '—',
|
|
89
|
-
}),
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
expect(lines).toContain(
|
|
93
|
-
'| keeweb | ❌ Keeweb exploded | ❌ Keeweb exploded | ❌ Keeweb exploded | ❌ Keeweb exploded | ❌ Keeweb exploded |',
|
|
94
|
-
)
|
|
95
|
-
expect(lines).toContain('| cliproxy | OK | — | v1.2.3 | — | 12 req / 0 fail |')
|
|
96
|
-
})
|
|
81
|
+
it('prints valid json with all app keys when --json is passed', async () => {
|
|
82
|
+
const {ctx, captured} = createCapturedCtx()
|
|
97
83
|
|
|
98
|
-
|
|
99
|
-
const lines = await runStatusCommand(['--json'], {
|
|
100
|
-
getKeewebStatusSummary: async () => ({
|
|
101
|
-
app: 'keeweb',
|
|
102
|
-
http: 'OK',
|
|
103
|
-
lastDeploy: '2026-04-12 10:00',
|
|
104
|
-
version: '—',
|
|
105
|
-
contentHash: 'match',
|
|
106
|
-
usageStats: '—',
|
|
107
|
-
}),
|
|
108
|
-
getCliproxyStatusSummary: async () => ({
|
|
109
|
-
app: 'cliproxy',
|
|
110
|
-
http: 'OK',
|
|
111
|
-
lastDeploy: '—',
|
|
112
|
-
version: 'v1.2.3',
|
|
113
|
-
contentHash: '—',
|
|
114
|
-
usageStats: '12 req / 0 fail',
|
|
115
|
-
}),
|
|
116
|
-
getGatewayStatusSummary: async () => ({
|
|
117
|
-
app: 'gateway',
|
|
118
|
-
http: 'OK: gateway:running/healthy',
|
|
119
|
-
lastDeploy: '—',
|
|
120
|
-
version: '—',
|
|
121
|
-
contentHash: '—',
|
|
122
|
-
usageStats: '—',
|
|
123
|
-
}),
|
|
124
|
-
})
|
|
84
|
+
await unifiedStatusAction({json: true}, ctx, makeDeps())
|
|
125
85
|
|
|
126
|
-
expect(
|
|
86
|
+
expect(captured.stdout).toHaveLength(1)
|
|
127
87
|
|
|
128
|
-
const [jsonOutput] =
|
|
88
|
+
const [jsonOutput] = captured.stdout
|
|
129
89
|
expect(jsonOutput).toBeDefined()
|
|
130
90
|
|
|
131
91
|
const parsed = JSON.parse(jsonOutput ?? '{}') as {
|
|
132
92
|
keeweb: {http: string}
|
|
133
93
|
cliproxy: {version: string}
|
|
94
|
+
gateway: {http: string}
|
|
134
95
|
}
|
|
135
96
|
|
|
136
97
|
expect(parsed.keeweb.http).toBe('OK')
|
|
137
98
|
expect(parsed.cliproxy.version).toBe('v1.2.3')
|
|
99
|
+
expect(parsed.gateway.http).toBe('OK: gateway:running/healthy')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('does not write to global console (output is captured via ctx)', async () => {
|
|
103
|
+
const {ctx, captured} = createCapturedCtx()
|
|
104
|
+
|
|
105
|
+
await unifiedStatusAction({}, ctx, makeDeps())
|
|
106
|
+
|
|
107
|
+
// Verify output went to ctx capture, not global console
|
|
108
|
+
expect(captured.stdout.length).toBeGreaterThan(0)
|
|
109
|
+
expect(expectCapturedToInclude(captured, '| App |')).toBe(true)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('top-level status command (registerStatus wiring)', () => {
|
|
114
|
+
it('wires through goke without throwing', async () => {
|
|
115
|
+
const cli = goke('test')
|
|
116
|
+
|
|
117
|
+
registerStatus(cli, makeDeps())
|
|
118
|
+
cli.parse(['bun', 'test', 'status'], {run: false})
|
|
119
|
+
|
|
120
|
+
// Verify the command runs without throwing (goke uses its own ctx for output)
|
|
121
|
+
await expect(cli.runMatchedCommand()).resolves.toBeUndefined()
|
|
138
122
|
})
|
|
139
123
|
})
|
package/src/commands/status.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type {goke} from 'goke'
|
|
2
2
|
|
|
3
|
+
import type {ActionCtx} from '../lib/action-ctx'
|
|
4
|
+
|
|
3
5
|
import {z} from 'zod'
|
|
4
6
|
|
|
5
7
|
import {getCliproxyStatusSummary} from './cliproxy/status'
|
|
@@ -69,6 +71,49 @@ function formatRow(row: StatusSummary): string {
|
|
|
69
71
|
return `| ${values.join(' | ')} |`
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
export async function unifiedStatusAction(
|
|
75
|
+
options: {json?: boolean; verbose?: boolean},
|
|
76
|
+
ctx: ActionCtx,
|
|
77
|
+
dependencies: StatusDependencies = {
|
|
78
|
+
getKeewebStatusSummary,
|
|
79
|
+
getCliproxyStatusSummary,
|
|
80
|
+
getGatewayStatusSummary,
|
|
81
|
+
},
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
const verbose = options.verbose === true
|
|
84
|
+
const cliproxyBaseUrl = stripTrailingSlash(process.env.CLIPROXY_URL ?? DEFAULT_CLIPROXY_URL)
|
|
85
|
+
const cliproxyKey = process.env.CLIPROXY_MANAGEMENT_KEY ?? ''
|
|
86
|
+
const gatewayHost = process.env.GATEWAY_HOST ?? ''
|
|
87
|
+
|
|
88
|
+
const results = await Promise.allSettled([
|
|
89
|
+
dependencies.getKeewebStatusSummary(verbose),
|
|
90
|
+
dependencies.getCliproxyStatusSummary(cliproxyBaseUrl, cliproxyKey, verbose),
|
|
91
|
+
dependencies.getGatewayStatusSummary(gatewayHost),
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
const appNames: AppName[] = ['keeweb', 'cliproxy', 'gateway']
|
|
95
|
+
const rows: StatusSummary[] = results.map((result, index) => {
|
|
96
|
+
const app = appNames[index] ?? 'keeweb'
|
|
97
|
+
if (result.status === 'fulfilled') {
|
|
98
|
+
return result.value
|
|
99
|
+
}
|
|
100
|
+
const reason = result.reason
|
|
101
|
+
const message = reason instanceof Error ? reason.message : String(reason)
|
|
102
|
+
ctx.console.error(`${app} status check failed: ${message}`)
|
|
103
|
+
return errorSummary(app, reason)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
if (options.json === true) {
|
|
107
|
+
ctx.console.log(JSON.stringify(toJsonPayload(rows)))
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
ctx.console.log('| App | HTTP | Last Deploy | Version | Content Hash | Usage Stats |')
|
|
112
|
+
for (const row of rows) {
|
|
113
|
+
ctx.console.log(formatRow(row))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
72
117
|
export function registerStatus(
|
|
73
118
|
cli: ReturnType<typeof goke>,
|
|
74
119
|
dependencies: StatusDependencies = {
|
|
@@ -87,32 +132,5 @@ export function registerStatus(
|
|
|
87
132
|
'--verbose',
|
|
88
133
|
z.boolean().describe('Include verbose per-app health check details when building the summary rows.'),
|
|
89
134
|
)
|
|
90
|
-
.action(
|
|
91
|
-
const verbose = options.verbose === true
|
|
92
|
-
const cliproxyBaseUrl = stripTrailingSlash(process.env.CLIPROXY_URL ?? DEFAULT_CLIPROXY_URL)
|
|
93
|
-
const cliproxyKey = process.env.CLIPROXY_MANAGEMENT_KEY ?? ''
|
|
94
|
-
const gatewayHost = process.env.GATEWAY_HOST ?? ''
|
|
95
|
-
|
|
96
|
-
const results = await Promise.allSettled([
|
|
97
|
-
dependencies.getKeewebStatusSummary(verbose),
|
|
98
|
-
dependencies.getCliproxyStatusSummary(cliproxyBaseUrl, cliproxyKey, verbose),
|
|
99
|
-
dependencies.getGatewayStatusSummary(gatewayHost),
|
|
100
|
-
])
|
|
101
|
-
|
|
102
|
-
const appNames: AppName[] = ['keeweb', 'cliproxy', 'gateway']
|
|
103
|
-
const rows: StatusSummary[] = results.map((result, index) => {
|
|
104
|
-
const app = appNames[index] ?? 'keeweb'
|
|
105
|
-
return result.status === 'fulfilled' ? result.value : errorSummary(app, result.reason)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
if (options.json === true) {
|
|
109
|
-
console.log(JSON.stringify(toJsonPayload(rows)))
|
|
110
|
-
return
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
console.log('| App | HTTP | Last Deploy | Version | Content Hash | Usage Stats |')
|
|
114
|
-
for (const row of rows) {
|
|
115
|
-
console.log(formatRow(row))
|
|
116
|
-
}
|
|
117
|
-
})
|
|
135
|
+
.action((options, ctx) => unifiedStatusAction(options, ctx, dependencies))
|
|
118
136
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal `ctx` interface every MCP-capturable action accepts.
|
|
3
|
+
*
|
|
4
|
+
* Structural subtype of goke's real `GokeExecutionContext` — actions
|
|
5
|
+
* consume only `console.{log,error}`, `process.{stdout,stderr}.write`,
|
|
6
|
+
* and `process.exit`, never the `fs` surface. Source files import this
|
|
7
|
+
* shape from this module so the action contract is defined in one place.
|
|
8
|
+
*
|
|
9
|
+
* The `write` parameter is `string` (narrower than goke's real
|
|
10
|
+
* `string | Uint8Array`) because every action passes string values;
|
|
11
|
+
* `CapturedCtx.write(string | Uint8Array)` is still assignable to
|
|
12
|
+
* `ActionCtx.write(string)` via contravariance, so tests using
|
|
13
|
+
* `CapturedCtx` satisfy this interface.
|
|
14
|
+
*/
|
|
15
|
+
export interface ActionCtx {
|
|
16
|
+
console: {
|
|
17
|
+
log: (...args: unknown[]) => void
|
|
18
|
+
error: (...args: unknown[]) => void
|
|
19
|
+
}
|
|
20
|
+
process: {
|
|
21
|
+
stdout: {write: (chunk: string) => void}
|
|
22
|
+
stderr: {write: (chunk: string) => void}
|
|
23
|
+
exit: (code: number) => never | void
|
|
24
|
+
}
|
|
25
|
+
}
|