@marcusrbrown/infra 0.12.0 → 0.12.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcusrbrown/infra",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Infrastructure management CLI — deploy automation, health checks, and MCP bridge",
5
5
  "keywords": [
6
6
  "infra",
@@ -167,6 +167,30 @@ describe('logs command', () => {
167
167
  })
168
168
  })
169
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
+
170
194
  // ─── SSH command includes repo-pinned UserKnownHostsFile ─────────────────────
171
195
 
172
196
  describe('streamDashboardLogs — SSH command includes UserKnownHostsFile', () => {
@@ -96,6 +96,8 @@ export async function streamDashboardLogs(
96
96
  '-o',
97
97
  'BatchMode=yes',
98
98
  '-o',
99
+ 'ConnectTimeout=10',
100
+ '-o',
99
101
  'StrictHostKeyChecking=yes',
100
102
  ...buildKnownHostsArgs(),
101
103
  ...identityArgs,
@@ -133,6 +133,40 @@ function makeSpawn(stdout: string, stderr = '', exitCode = 0): SpawnFn {
133
133
  }
134
134
  }
135
135
 
136
+ // ─── SSH command includes ConnectTimeout ─────────────────────────────────────
137
+
138
+ describe('getDashboardComposeStatus — SSH command includes ConnectTimeout', () => {
139
+ it('passes -o ConnectTimeout=10 to ssh', async () => {
140
+ let capturedCmd: string[] = []
141
+
142
+ const capturingSpawn: SpawnFn = (cmd, _opts) => {
143
+ capturedCmd = cmd
144
+ const enc = new TextEncoder()
145
+ return {
146
+ stdout: new ReadableStream({
147
+ start(c) {
148
+ c.enqueue(enc.encode(JSON.stringify([{Name: 'dashboard', State: 'running', Health: 'healthy'}])))
149
+ c.close()
150
+ },
151
+ }),
152
+ stderr: new ReadableStream({
153
+ start(c) {
154
+ c.close()
155
+ },
156
+ }),
157
+ exited: Promise.resolve(0),
158
+ }
159
+ }
160
+
161
+ await getDashboardComposeStatus('dashboard.fro.bot', capturingSpawn)
162
+
163
+ const connectTimeoutIdx = capturedCmd.findIndex(arg => arg.startsWith('ConnectTimeout='))
164
+ expect(connectTimeoutIdx).toBeGreaterThan(-1)
165
+ expect(capturedCmd[connectTimeoutIdx - 1]).toBe('-o')
166
+ expect(capturedCmd[connectTimeoutIdx]).toBe('ConnectTimeout=10')
167
+ })
168
+ })
169
+
136
170
  // ─── SSH command includes repo-pinned UserKnownHostsFile ─────────────────────
137
171
 
138
172
  describe('getDashboardComposeStatus — SSH command includes UserKnownHostsFile', () => {
@@ -362,6 +396,28 @@ describe('status command', () => {
362
396
  expect(captured.exit?.code).toBe(1)
363
397
  })
364
398
 
399
+ it('prints the SSH error message (not a generic no-services message) when SSH fails', async () => {
400
+ process.env.DASHBOARD_DOMAIN = 'dashboard.fro.bot'
401
+
402
+ const {ctx, captured} = createCapturedCtx()
403
+
404
+ try {
405
+ await dashboardStatusAction(
406
+ {},
407
+ ctx,
408
+ makeSpawn('', 'ssh: connect to host dashboard.fro.bot port 22: Connection timed out', 255),
409
+ )
410
+ } catch (error) {
411
+ if (!(error instanceof MockProcessExit)) throw error
412
+ }
413
+
414
+ const stderrText = captured.stderr.join('')
415
+ // Must surface the SSH failure detail, not the generic no-services fallback
416
+ expect(stderrText).toContain('SSH command failed')
417
+ expect(stderrText).not.toContain('No services reported by docker compose ps')
418
+ expect(captured.exit?.code).toBe(1)
419
+ })
420
+
365
421
  it('calls ctx.console.error and exits when DASHBOARD_DOMAIN is not set', async () => {
366
422
  delete process.env.DASHBOARD_DOMAIN
367
423
 
@@ -109,6 +109,8 @@ export async function getDashboardComposeStatus(
109
109
  '-o',
110
110
  'BatchMode=yes',
111
111
  '-o',
112
+ 'ConnectTimeout=10',
113
+ '-o',
112
114
  'StrictHostKeyChecking=yes',
113
115
  ...knownHostsArgs,
114
116
  ...identityArgs,
@@ -250,7 +252,8 @@ export async function dashboardStatusAction(
250
252
  }
251
253
 
252
254
  if (result.services.length === 0) {
253
- ctx.console.error(`Error: No services reported by docker compose ps`)
255
+ const msg = result.error ?? 'No services reported by docker compose ps'
256
+ ctx.console.error(`Error: ${msg}`)
254
257
  ctx.process.exit(1)
255
258
  return
256
259
  }