@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.
Files changed (108) hide show
  1. package/dist/cli.js +4977 -0
  2. package/package.json +7 -3
  3. package/src/__snapshots__/cli.test.ts.snap +0 -219
  4. package/src/__test__/mcp-ctx-fixture.test.ts +0 -108
  5. package/src/__test__/mcp-ctx-fixture.ts +0 -104
  6. package/src/cli.test.ts +0 -66
  7. package/src/cli.ts +0 -43
  8. package/src/commands/cliproxy/config.test.ts +0 -475
  9. package/src/commands/cliproxy/config.ts +0 -238
  10. package/src/commands/cliproxy/deploy.test.ts +0 -181
  11. package/src/commands/cliproxy/deploy.ts +0 -157
  12. package/src/commands/cliproxy/host.test.ts +0 -73
  13. package/src/commands/cliproxy/host.ts +0 -31
  14. package/src/commands/cliproxy/index.ts +0 -21
  15. package/src/commands/cliproxy/keys.test.ts +0 -326
  16. package/src/commands/cliproxy/keys.ts +0 -201
  17. package/src/commands/cliproxy/login.test.ts +0 -415
  18. package/src/commands/cliproxy/login.ts +0 -131
  19. package/src/commands/cliproxy/models.test.ts +0 -592
  20. package/src/commands/cliproxy/models.ts +0 -237
  21. package/src/commands/cliproxy/open.test.ts +0 -132
  22. package/src/commands/cliproxy/open.ts +0 -78
  23. package/src/commands/cliproxy/setup/gh.test.ts +0 -218
  24. package/src/commands/cliproxy/setup/gh.ts +0 -250
  25. package/src/commands/cliproxy/setup/preview.test.ts +0 -159
  26. package/src/commands/cliproxy/setup/preview.ts +0 -41
  27. package/src/commands/cliproxy/setup/prompts.test.ts +0 -58
  28. package/src/commands/cliproxy/setup/prompts.ts +0 -99
  29. package/src/commands/cliproxy/setup/providers.test.ts +0 -245
  30. package/src/commands/cliproxy/setup/providers.ts +0 -136
  31. package/src/commands/cliproxy/setup/smoke-test.test.ts +0 -821
  32. package/src/commands/cliproxy/setup/smoke-test.ts +0 -223
  33. package/src/commands/cliproxy/setup/templates.test.ts +0 -357
  34. package/src/commands/cliproxy/setup/templates.ts +0 -145
  35. package/src/commands/cliproxy/setup/validation.test.ts +0 -567
  36. package/src/commands/cliproxy/setup/validation.ts +0 -209
  37. package/src/commands/cliproxy/setup/workflow-analyzer.test.ts +0 -363
  38. package/src/commands/cliproxy/setup/workflow-analyzer.ts +0 -136
  39. package/src/commands/cliproxy/setup.test.ts +0 -2366
  40. package/src/commands/cliproxy/setup.ts +0 -809
  41. package/src/commands/cliproxy/shared.test.ts +0 -118
  42. package/src/commands/cliproxy/shared.ts +0 -84
  43. package/src/commands/cliproxy/status.test.ts +0 -1157
  44. package/src/commands/cliproxy/status.ts +0 -597
  45. package/src/commands/dashboard/deploy.test.ts +0 -215
  46. package/src/commands/dashboard/deploy.ts +0 -135
  47. package/src/commands/dashboard/host.test.ts +0 -79
  48. package/src/commands/dashboard/host.ts +0 -33
  49. package/src/commands/dashboard/index.ts +0 -13
  50. package/src/commands/dashboard/logs.test.ts +0 -317
  51. package/src/commands/dashboard/logs.ts +0 -180
  52. package/src/commands/dashboard/status.test.ts +0 -708
  53. package/src/commands/dashboard/status.ts +0 -293
  54. package/src/commands/gateway/backup.test.ts +0 -426
  55. package/src/commands/gateway/backup.ts +0 -205
  56. package/src/commands/gateway/deploy.test.ts +0 -491
  57. package/src/commands/gateway/deploy.ts +0 -173
  58. package/src/commands/gateway/host.test.ts +0 -89
  59. package/src/commands/gateway/host.ts +0 -33
  60. package/src/commands/gateway/index.ts +0 -17
  61. package/src/commands/gateway/logs.test.ts +0 -263
  62. package/src/commands/gateway/logs.ts +0 -160
  63. package/src/commands/gateway/restore.test.ts +0 -650
  64. package/src/commands/gateway/restore.ts +0 -308
  65. package/src/commands/gateway/status.test.ts +0 -755
  66. package/src/commands/gateway/status.ts +0 -255
  67. package/src/commands/keeweb/deploy.test.ts +0 -177
  68. package/src/commands/keeweb/deploy.ts +0 -191
  69. package/src/commands/keeweb/index.ts +0 -11
  70. package/src/commands/keeweb/open.test.ts +0 -54
  71. package/src/commands/keeweb/open.ts +0 -30
  72. package/src/commands/keeweb/status.test.ts +0 -509
  73. package/src/commands/keeweb/status.ts +0 -359
  74. package/src/commands/mcp.test.ts +0 -260
  75. package/src/commands/mcp.ts +0 -58
  76. package/src/commands/status.test.ts +0 -222
  77. package/src/commands/status.ts +0 -161
  78. package/src/commands/umami/deploy.test.ts +0 -202
  79. package/src/commands/umami/deploy.ts +0 -132
  80. package/src/commands/umami/host.test.ts +0 -79
  81. package/src/commands/umami/host.ts +0 -33
  82. package/src/commands/umami/index.ts +0 -13
  83. package/src/commands/umami/logs.test.ts +0 -191
  84. package/src/commands/umami/logs.ts +0 -163
  85. package/src/commands/umami/status.test.ts +0 -633
  86. package/src/commands/umami/status.ts +0 -285
  87. package/src/commands/vpn/client.test.ts +0 -949
  88. package/src/commands/vpn/client.ts +0 -467
  89. package/src/commands/vpn/deploy.test.ts +0 -208
  90. package/src/commands/vpn/deploy.ts +0 -143
  91. package/src/commands/vpn/host.test.ts +0 -89
  92. package/src/commands/vpn/host.ts +0 -36
  93. package/src/commands/vpn/index.ts +0 -15
  94. package/src/commands/vpn/logs.test.ts +0 -234
  95. package/src/commands/vpn/logs.ts +0 -136
  96. package/src/commands/vpn/peers.test.ts +0 -478
  97. package/src/commands/vpn/shared.test.ts +0 -454
  98. package/src/commands/vpn/shared.ts +0 -170
  99. package/src/commands/vpn/status.test.ts +0 -142
  100. package/src/commands/vpn/status.ts +0 -78
  101. package/src/conventions.test.ts +0 -1180
  102. package/src/lib/action-ctx.ts +0 -25
  103. package/src/lib/known-hosts.test.ts +0 -192
  104. package/src/lib/known-hosts.ts +0 -76
  105. package/src/lib/redact.ts +0 -21
  106. package/src/lib/ssh-identity.test.ts +0 -134
  107. package/src/lib/ssh-identity.ts +0 -76
  108. /package/{src/resources/known_hosts → dist/known_hosts-tk724cp0.} +0 -0
@@ -1,317 +0,0 @@
1
- import {afterEach, beforeEach, describe, expect, it} from 'bun:test'
2
-
3
- import {streamDashboardLogs, type LogsSpawnFn, type StreamLogsOpts} from './logs'
4
-
5
- // ─── Capturing spawn helper ───────────────────────────────────────────────────
6
-
7
- function makeCapturingLogsSpawn(exitCode = 0): {spawn: LogsSpawnFn; capturedCmd: () => string[]} {
8
- let captured: string[] = []
9
- const spawn: LogsSpawnFn = (cmd, _opts) => {
10
- captured = [...cmd]
11
- return {exited: Promise.resolve(exitCode)}
12
- }
13
- return {spawn, capturedCmd: () => captured}
14
- }
15
-
16
- // NOTE: dashboard logs is intentionally CLI-only and NOT in the MCP allowlist.
17
- // Logs may contain sensitive data (DB passwords, app secrets, user data).
18
-
19
- // ─── streamDashboardLogs ─────────────────────────────────────────────────────
20
-
21
- function makeLogsSpawn(exitCode = 0): LogsSpawnFn {
22
- return (_cmd, _opts) => ({exited: Promise.resolve(exitCode)})
23
- }
24
-
25
- describe('logs command', () => {
26
- let originalCI: string | undefined
27
- let originalDashboardDomain: string | undefined
28
-
29
- beforeEach(() => {
30
- originalCI = process.env.CI
31
- originalDashboardDomain = process.env.DASHBOARD_DOMAIN
32
- })
33
-
34
- afterEach(() => {
35
- if (originalCI === undefined) {
36
- delete process.env.CI
37
- } else {
38
- process.env.CI = originalCI
39
- }
40
-
41
- if (originalDashboardDomain === undefined) {
42
- delete process.env.DASHBOARD_DOMAIN
43
- } else {
44
- process.env.DASHBOARD_DOMAIN = originalDashboardDomain
45
- }
46
- })
47
-
48
- it('refuses to stream logs in CI without --allow-ci', async () => {
49
- process.env.CI = 'true'
50
-
51
- const opts: StreamLogsOpts = {
52
- host: 'dashboard.fro.bot',
53
- service: 'dashboard',
54
- tail: 100,
55
- allowCi: false,
56
- }
57
-
58
- const messages: string[] = []
59
- const result = await streamDashboardLogs(opts, makeLogsSpawn(), msg => messages.push(msg))
60
-
61
- expect(result.refused).toBe(true)
62
- expect(messages.join('')).toContain('Refusing to stream logs in CI')
63
- })
64
-
65
- it('proceeds in CI when --allow-ci is set', async () => {
66
- process.env.CI = 'true'
67
-
68
- const opts: StreamLogsOpts = {
69
- host: 'dashboard.fro.bot',
70
- service: 'dashboard',
71
- tail: 100,
72
- allowCi: true,
73
- }
74
-
75
- const warnings: string[] = []
76
- const result = await streamDashboardLogs(opts, makeLogsSpawn(), undefined, msg => warnings.push(msg))
77
-
78
- expect(result.refused).toBe(false)
79
- expect(result.exitCode).toBe(0)
80
- })
81
-
82
- it('always emits a sensitive-data warning to stderr', async () => {
83
- delete process.env.CI
84
-
85
- const opts: StreamLogsOpts = {
86
- host: 'dashboard.fro.bot',
87
- service: 'dashboard',
88
- tail: 100,
89
- allowCi: false,
90
- }
91
-
92
- const warnings: string[] = []
93
- await streamDashboardLogs(opts, makeLogsSpawn(), undefined, msg => warnings.push(msg))
94
-
95
- expect(warnings.join('')).toContain('sensitive')
96
- })
97
-
98
- it('rejects invalid host before spawning SSH', async () => {
99
- delete process.env.CI
100
-
101
- const opts: StreamLogsOpts = {
102
- host: '-oProxyCommand=evil',
103
- service: 'dashboard',
104
- tail: 100,
105
- allowCi: false,
106
- }
107
-
108
- let spawnCalled = false
109
- const spy: LogsSpawnFn = (_cmd, _opts) => {
110
- spawnCalled = true
111
- return {exited: Promise.resolve(0)}
112
- }
113
-
114
- await expect(streamDashboardLogs(opts, spy)).rejects.toThrow('Invalid DASHBOARD_DOMAIN')
115
- expect(spawnCalled).toBe(false)
116
- })
117
-
118
- it('returns exitCode from the SSH subprocess', async () => {
119
- delete process.env.CI
120
-
121
- const opts: StreamLogsOpts = {
122
- host: 'dashboard.fro.bot',
123
- service: 'dashboard',
124
- tail: 50,
125
- allowCi: false,
126
- }
127
-
128
- const result = await streamDashboardLogs(opts, makeLogsSpawn(42))
129
-
130
- expect(result.refused).toBe(false)
131
- expect(result.exitCode).toBe(42)
132
- })
133
-
134
- it('accepts caddy as a valid service', async () => {
135
- delete process.env.CI
136
-
137
- const opts: StreamLogsOpts = {
138
- host: 'dashboard.fro.bot',
139
- service: 'caddy',
140
- tail: 100,
141
- allowCi: false,
142
- }
143
-
144
- const result = await streamDashboardLogs(opts, makeLogsSpawn(0))
145
- expect(result.refused).toBe(false)
146
- expect(result.exitCode).toBe(0)
147
- })
148
-
149
- it('rejects an unknown service when called directly (bypassing the CLI action)', async () => {
150
- delete process.env.CI
151
-
152
- const opts: StreamLogsOpts = {
153
- host: 'dashboard.fro.bot',
154
- service: 'notaservice',
155
- tail: 100,
156
- allowCi: false,
157
- }
158
-
159
- let spawnCalled = false
160
- const spy: LogsSpawnFn = (_cmd, _opts) => {
161
- spawnCalled = true
162
- return {exited: Promise.resolve(0)}
163
- }
164
-
165
- await expect(streamDashboardLogs(opts, spy)).rejects.toThrow(/Invalid service/)
166
- expect(spawnCalled).toBe(false)
167
- })
168
- })
169
-
170
- // ─── SSH command includes ConnectTimeout ─────────────────────────────────────
171
-
172
- describe('streamDashboardLogs — SSH command includes ConnectTimeout', () => {
173
- it('passes -o ConnectTimeout=10 to ssh', async () => {
174
- delete process.env.CI
175
-
176
- let capturedCmd: string[] = []
177
- const spawnCapture: LogsSpawnFn = (cmd, _opts) => {
178
- capturedCmd = cmd
179
- return {exited: Promise.resolve(0)}
180
- }
181
-
182
- await streamDashboardLogs(
183
- {host: 'dashboard.fro.bot', service: 'dashboard', tail: 100, allowCi: false},
184
- spawnCapture,
185
- )
186
-
187
- const connectTimeoutIdx = capturedCmd.findIndex(arg => arg.startsWith('ConnectTimeout='))
188
- expect(connectTimeoutIdx).toBeGreaterThan(-1)
189
- expect(capturedCmd[connectTimeoutIdx - 1]).toBe('-o')
190
- expect(capturedCmd[connectTimeoutIdx]).toBe('ConnectTimeout=10')
191
- })
192
- })
193
-
194
- // ─── SSH command includes repo-pinned UserKnownHostsFile ─────────────────────
195
-
196
- describe('streamDashboardLogs — SSH command includes UserKnownHostsFile', () => {
197
- it('passes -o UserKnownHostsFile=<repo>/.github/known_hosts to ssh', async () => {
198
- delete process.env.CI
199
-
200
- let capturedCmd: string[] = []
201
- const spawnCapture: LogsSpawnFn = (cmd, _opts) => {
202
- capturedCmd = cmd
203
- return {exited: Promise.resolve(0)}
204
- }
205
-
206
- await streamDashboardLogs(
207
- {host: 'dashboard.fro.bot', service: 'dashboard', tail: 100, allowCi: false},
208
- spawnCapture,
209
- )
210
-
211
- const knownHostsIdx = capturedCmd.findIndex(arg => arg.startsWith('UserKnownHostsFile='))
212
- expect(knownHostsIdx).toBeGreaterThan(-1)
213
- expect(capturedCmd[knownHostsIdx - 1]).toBe('-o')
214
- expect(capturedCmd[knownHostsIdx]).toMatch(/\.github[/\\]known_hosts$/)
215
- })
216
-
217
- it('does not weaken StrictHostKeyChecking when UserKnownHostsFile is added', async () => {
218
- delete process.env.CI
219
-
220
- let capturedCmd: string[] = []
221
- const spawnCapture: LogsSpawnFn = (cmd, _opts) => {
222
- capturedCmd = cmd
223
- return {exited: Promise.resolve(0)}
224
- }
225
-
226
- await streamDashboardLogs(
227
- {host: 'dashboard.fro.bot', service: 'dashboard', tail: 100, allowCi: false},
228
- spawnCapture,
229
- )
230
-
231
- const strictIdx = capturedCmd.findIndex(arg => arg.startsWith('StrictHostKeyChecking='))
232
- expect(strictIdx).toBeGreaterThan(-1)
233
- expect(capturedCmd[strictIdx]).toBe('StrictHostKeyChecking=yes')
234
- })
235
- })
236
-
237
- // ─── SSH identity injection (DASHBOARD_SSH_KEY) ───────────────────────────────
238
-
239
- describe('streamDashboardLogs — SSH identity injection', () => {
240
- let originalEnv: Record<string, string | undefined>
241
-
242
- beforeEach(() => {
243
- originalEnv = {
244
- CI: process.env.CI,
245
- DASHBOARD_SSH_KEY: process.env.DASHBOARD_SSH_KEY,
246
- }
247
- delete process.env.CI
248
- })
249
-
250
- afterEach(() => {
251
- if (originalEnv.CI === undefined) {
252
- delete process.env.CI
253
- } else {
254
- process.env.CI = originalEnv.CI
255
- }
256
-
257
- if (originalEnv.DASHBOARD_SSH_KEY === undefined) {
258
- delete process.env.DASHBOARD_SSH_KEY
259
- } else {
260
- process.env.DASHBOARD_SSH_KEY = originalEnv.DASHBOARD_SSH_KEY
261
- }
262
- })
263
-
264
- it('includes -i <path> and IdentitiesOnly=yes in ssh argv when DASHBOARD_SSH_KEY is set', async () => {
265
- process.env.DASHBOARD_SSH_KEY = '-----BEGIN OPENSSH PRIVATE KEY-----\nfakekey\n-----END OPENSSH PRIVATE KEY-----\n'
266
-
267
- const {spawn, capturedCmd} = makeCapturingLogsSpawn(0)
268
-
269
- await streamDashboardLogs({host: 'dashboard.fro.bot', service: 'dashboard', tail: 100, allowCi: false}, spawn)
270
-
271
- const cmd = capturedCmd()
272
- const iIdx = cmd.indexOf('-i')
273
- expect(iIdx).toBeGreaterThan(-1)
274
- expect(cmd[iIdx + 1]).toBeTruthy()
275
-
276
- const identitiesOnlyIdx = cmd.indexOf('IdentitiesOnly=yes')
277
- expect(identitiesOnlyIdx).toBeGreaterThan(-1)
278
-
279
- const destination = cmd.find(arg => arg.includes('@'))
280
- expect(destination).toBe('root@dashboard.fro.bot')
281
- })
282
-
283
- it('does not include -i or IdentitiesOnly=yes when DASHBOARD_SSH_KEY is absent', async () => {
284
- delete process.env.DASHBOARD_SSH_KEY
285
-
286
- const {spawn, capturedCmd} = makeCapturingLogsSpawn(0)
287
-
288
- await streamDashboardLogs({host: 'dashboard.fro.bot', service: 'dashboard', tail: 100, allowCi: false}, spawn)
289
-
290
- const cmd = capturedCmd()
291
- expect(cmd.indexOf('-i')).toBe(-1)
292
- expect(cmd.indexOf('IdentitiesOnly=yes')).toBe(-1)
293
- })
294
-
295
- it('cleans up the temp key file after the SSH command completes', async () => {
296
- const {statSync} = await import('node:fs')
297
- process.env.DASHBOARD_SSH_KEY = '-----BEGIN OPENSSH PRIVATE KEY-----\nfakekey\n-----END OPENSSH PRIVATE KEY-----\n'
298
-
299
- let capturedKeyPath: string | undefined
300
- const capturingSpawn: LogsSpawnFn = (cmd, _opts) => {
301
- const iIdx = cmd.indexOf('-i')
302
- if (iIdx !== -1) capturedKeyPath = cmd[iIdx + 1]
303
- return {exited: Promise.resolve(0)}
304
- }
305
-
306
- await streamDashboardLogs(
307
- {host: 'dashboard.fro.bot', service: 'dashboard', tail: 100, allowCi: false},
308
- capturingSpawn,
309
- )
310
-
311
- expect(capturedKeyPath).toBeTruthy()
312
- const keyPath = capturedKeyPath
313
- if (keyPath) {
314
- expect(() => statSync(keyPath)).toThrow()
315
- }
316
- })
317
- })
@@ -1,180 +0,0 @@
1
- import type {goke} from 'goke'
2
-
3
- import {z} from 'zod'
4
-
5
- import {buildKnownHostsArgs} from '../../lib/known-hosts'
6
- import {buildIdentityArgs} from '../../lib/ssh-identity'
7
- import {validateDashboardHost} from './host'
8
-
9
- declare const process: {
10
- env: Record<string, string | undefined>
11
- exitCode?: number
12
- stderr: {write: (msg: string) => void}
13
- }
14
-
15
- const COMPOSE_PROJECT_DIR = '/opt/dashboard'
16
- const SENSITIVE_WARNING =
17
- 'Warning: Logs may contain database passwords, app secrets, or user data. Treat output as sensitive; do not capture in shared logs or chat.'
18
-
19
- export const VALID_SERVICES = ['dashboard', 'caddy'] as const
20
-
21
- export type ValidService = (typeof VALID_SERVICES)[number]
22
-
23
- // ─── Pure helpers ─────────────────────────────────────────────────────────────
24
-
25
- export function validateService(service: string): asserts service is ValidService {
26
- if (!VALID_SERVICES.includes(service as ValidService)) {
27
- throw new Error(`Invalid service "${service}". Valid services: ${VALID_SERVICES.join(', ')}`)
28
- }
29
- }
30
-
31
- // ─── Injectable spawn type ────────────────────────────────────────────────────
32
-
33
- export type LogsSpawnFn = (
34
- cmd: string[],
35
- opts: {env: Record<string, string>; stdout: 'inherit'; stderr: 'inherit'},
36
- ) => {
37
- exited: Promise<number>
38
- }
39
-
40
- export interface StreamLogsOpts {
41
- host: string
42
- service: string
43
- tail: number
44
- allowCi: boolean
45
- }
46
-
47
- export interface StreamLogsResult {
48
- refused: boolean
49
- exitCode?: number
50
- }
51
-
52
- function defaultLogsSpawn(
53
- cmd: string[],
54
- opts: {env: Record<string, string>; stdout: 'inherit'; stderr: 'inherit'},
55
- ): {exited: Promise<number>} {
56
- return Bun.spawn(cmd, opts)
57
- }
58
-
59
- // NOTE: dashboard logs is intentionally CLI-only and NOT in the MCP allowlist.
60
- // Logs may contain sensitive data (DB passwords, app secrets, user data).
61
- export async function streamDashboardLogs(
62
- opts: StreamLogsOpts,
63
- spawn: LogsSpawnFn = defaultLogsSpawn,
64
- printOut?: (msg: string) => void,
65
- printErr?: (msg: string) => void,
66
- ): Promise<StreamLogsResult> {
67
- // Validate service at the top so direct callers cannot bypass the guard.
68
- validateService(opts.service)
69
-
70
- const isCI = process.env.CI === 'true'
71
-
72
- if (isCI && !opts.allowCi) {
73
- const msg = 'Refusing to stream logs in CI without --allow-ci. Logs may contain sensitive credentials or user data.'
74
- if (printOut) {
75
- printOut(msg)
76
- } else {
77
- console.log(msg)
78
- }
79
-
80
- return {refused: true}
81
- }
82
-
83
- // Warn on every run — logs are sensitive regardless of context
84
- if (printErr) {
85
- printErr(SENSITIVE_WARNING)
86
- } else {
87
- console.error(SENSITIVE_WARNING)
88
- }
89
-
90
- validateDashboardHost(opts.host)
91
-
92
- const {args: identityArgs, cleanup} = buildIdentityArgs(process.env.DASHBOARD_SSH_KEY)
93
-
94
- const sshCmd = [
95
- 'ssh',
96
- '-o',
97
- 'BatchMode=yes',
98
- '-o',
99
- 'ConnectTimeout=10',
100
- '-o',
101
- 'StrictHostKeyChecking=yes',
102
- ...buildKnownHostsArgs(),
103
- ...identityArgs,
104
- `root@${opts.host}`,
105
- `docker compose --project-directory ${COMPOSE_PROJECT_DIR} logs --no-color --tail=${opts.tail} ${opts.service}`,
106
- ]
107
-
108
- const env: Record<string, string> = {
109
- PATH: process.env.PATH ?? '/usr/bin:/bin',
110
- HOME: process.env.HOME ?? '/tmp',
111
- ...(process.env.SSH_AUTH_SOCK ? {SSH_AUTH_SOCK: process.env.SSH_AUTH_SOCK} : {}),
112
- }
113
-
114
- let exitCode: number
115
- try {
116
- const child = spawn(sshCmd, {env, stdout: 'inherit', stderr: 'inherit'})
117
- exitCode = await child.exited
118
- } finally {
119
- cleanup()
120
- }
121
-
122
- return {refused: false, exitCode}
123
- }
124
-
125
- // ─── Command registration ─────────────────────────────────────────────────────
126
-
127
- export function registerDashboardLogs(cli: ReturnType<typeof goke>): void {
128
- cli
129
- .command('dashboard logs [service]', 'Stream logs from a dashboard service via SSH and docker compose.')
130
- .option('--tail [n]', z.number().default(100).describe('Number of log lines to tail from each service.'))
131
- .option(
132
- '--allow-ci',
133
- z
134
- .boolean()
135
- .default(false)
136
- .describe('Allow log streaming in CI environments. Logs may contain sensitive credentials.'),
137
- )
138
- .option(
139
- '--key [key]',
140
- z
141
- .string()
142
- .describe('Environment variable name holding the SSH host. Falls back to DASHBOARD_DOMAIN when omitted.'),
143
- )
144
- .action(async (service, options) => {
145
- const targetService = (service as string | undefined) ?? 'dashboard'
146
-
147
- try {
148
- validateService(targetService)
149
- } catch (error) {
150
- console.error(error instanceof Error ? error.message : String(error))
151
- process.exitCode = 1
152
- return
153
- }
154
-
155
- const hostEnvKey = options.key ?? 'DASHBOARD_DOMAIN'
156
- const host = process.env[hostEnvKey]
157
-
158
- if (!host) {
159
- console.error(
160
- 'Dashboard host not set. Export DASHBOARD_DOMAIN or pass --key <env-name> pointing to a set variable.',
161
- )
162
- process.exitCode = 1
163
- return
164
- }
165
-
166
- const tail = typeof options.tail === 'number' ? options.tail : 100
167
- const allowCi = options.allowCi === true
168
-
169
- const result = await streamDashboardLogs({host, service: targetService, tail, allowCi})
170
-
171
- if (result.refused) {
172
- process.exitCode = 1
173
- return
174
- }
175
-
176
- if (result.exitCode !== 0) {
177
- process.exitCode = result.exitCode ?? 1
178
- }
179
- })
180
- }