From c63f3c22701f788430a4e104ed8c056bbabb11d6 Mon Sep 17 00:00:00 2001 From: Tom Riglar Date: Sat, 13 Jun 2026 08:37:30 +0100 Subject: [PATCH 1/2] refactor: route live session commands through ApiGateway The five live subcommands (start/install/exec/stop/status) each built their own fetch() with hand-spread auth.headers and per-call handleApiError, the one architecture violation flagged in the review (CLAUDE.md: commands orchestrate services, no I/O logic). They also bypassed ApiGateway's network-error enhancement, so a DNS/connection failure surfaced as a bare "fetch failed" TypeError instead of the friendly diagnostic every other command gives. Adds startLiveSession/installLiveBinary/execLiveYaml/stopLiveSession/ getLiveSession to ApiGateway (with typed LiveSession/LiveSessionSummary/ LiveExecResult interfaces, since the swagger has no /live schema), each following the existing try/fetch/handleApiError/enhanceFetchError skeleton. live.ts keeps all its presentation logic and just calls the gateway. Net -66 lines in the command. Co-Authored-By: Claude Fable 5 --- src/commands/live.ts | 80 ++---------------- src/gateways/api-gateway.ts | 158 ++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 73 deletions(-) diff --git a/src/commands/live.ts b/src/commands/live.ts index 081c9bc..544f592 100644 --- a/src/commands/live.ts +++ b/src/commands/live.ts @@ -41,26 +41,11 @@ const startSub = defineCommand({ logger.log(`${symbols.running} Starting ${platform} live session...`); - const res = await fetch(`${apiUrl}/live`, { - body: JSON.stringify({ binaryUploadId: binaryId, platform }), - headers: { - 'content-type': 'application/json', - ...auth.headers, - }, - method: 'POST', + const session = await ApiGateway.startLiveSession(apiUrl, auth, { + binaryUploadId: binaryId, + platform, }); - if (!res.ok) { - await ApiGateway.handleApiError(res, 'Failed to start live session'); - } - - const session = (await res.json()) as { - id: number; - platform: string; - session_name: string; - status: string; - }; - const frontendUrl = resolveFrontendUrl(apiUrl); logger.log(`${symbols.success} Live session started`); @@ -104,18 +89,7 @@ const installSub = defineCommand({ `${symbols.running} Installing binary ${colors.highlight(binaryId)} on session ${colors.highlight(sessionName)}...`, ); - const res = await fetch(`${apiUrl}/live/${sessionName}/install`, { - body: JSON.stringify({ binaryUploadId: binaryId }), - headers: { - 'content-type': 'application/json', - ...auth.headers, - }, - method: 'POST', - }); - - if (!res.ok) { - await ApiGateway.handleApiError(res, 'Failed to install binary'); - } + await ApiGateway.installLiveBinary(apiUrl, auth, sessionName, binaryId); logger.log(`${symbols.success} Binary installed successfully`); }, @@ -138,24 +112,7 @@ const execSub = defineCommand({ `${symbols.running} Executing commands on session ${colors.highlight(sessionName)}...`, ); - const res = await fetch(`${apiUrl}/live/${sessionName}/exec`, { - body: JSON.stringify({ yaml }), - headers: { - 'content-type': 'application/json', - ...auth.headers, - }, - method: 'POST', - }); - - if (!res.ok) { - await ApiGateway.handleApiError(res, 'Failed to execute test'); - } - - const result = (await res.json()) as { - error?: string; - output?: string; - success: boolean; - }; + const result = await ApiGateway.execLiveYaml(apiUrl, auth, sessionName, yaml); logger.log( result.success @@ -188,14 +145,7 @@ const stopSub = defineCommand({ logger.log(`${symbols.running} Stopping session ${colors.highlight(sessionName)}...`); - const res = await fetch(`${apiUrl}/live/${sessionName}`, { - headers: { ...auth.headers }, - method: 'DELETE', - }); - - if (!res.ok) { - await ApiGateway.handleApiError(res, 'Failed to stop session'); - } + await ApiGateway.stopLiveSession(apiUrl, auth, sessionName); logger.log(`${symbols.success} Session stopped`); }, @@ -212,23 +162,7 @@ const statusSub = defineCommand({ const apiUrl = args['api-url'] as string; const sessionName = args.session as string; - const res = await fetch(`${apiUrl}/live/${sessionName}`, { - headers: { ...auth.headers }, - method: 'GET', - }); - - if (!res.ok) { - await ApiGateway.handleApiError(res, 'Failed to get session status'); - } - - const session = (await res.json()) as { - binary_upload_id: null | string; - created_at: string; - id: number; - platform: string; - session_name: string; - status: string; - }; + const session = await ApiGateway.getLiveSession(apiUrl, auth, sessionName); logger.log(sectionHeader('Live Session')); logger.log(` ${colors.dim('Session:')} ${colors.highlight(session.session_name)}`); diff --git a/src/gateways/api-gateway.ts b/src/gateways/api-gateway.ts index 022d9bc..c3a9e99 100644 --- a/src/gateways/api-gateway.ts +++ b/src/gateways/api-gateway.ts @@ -37,6 +37,27 @@ async function parseJsonResponse(res: Response, operation: string): Promise { + try { + const res = await fetch(`${baseUrl}/live`, { + body: JSON.stringify({ + binaryUploadId: params.binaryUploadId, + platform: params.platform, + }), + headers: { + 'content-type': 'application/json', + ...auth.headers, + }, + method: 'POST', + }); + if (!res.ok) { + await this.handleApiError(res, 'Failed to start live session'); + } + + return await parseJsonResponse(res, 'Failed to start live session'); + } catch (error) { + if (error instanceof TypeError && error.message === 'fetch failed') { + throw this.enhanceFetchError(error, `${baseUrl}/live`); + } + + throw error; + } + }, + + async installLiveBinary( + baseUrl: string, + auth: AuthContext, + sessionName: string, + binaryUploadId: string, + ): Promise { + const url = `${baseUrl}/live/${sessionName}/install`; + try { + const res = await fetch(url, { + body: JSON.stringify({ binaryUploadId }), + headers: { + 'content-type': 'application/json', + ...auth.headers, + }, + method: 'POST', + }); + if (!res.ok) { + await this.handleApiError(res, 'Failed to install binary'); + } + } catch (error) { + if (error instanceof TypeError && error.message === 'fetch failed') { + throw this.enhanceFetchError(error, url); + } + + throw error; + } + }, + + async execLiveYaml( + baseUrl: string, + auth: AuthContext, + sessionName: string, + yaml: string, + ): Promise { + const url = `${baseUrl}/live/${sessionName}/exec`; + try { + const res = await fetch(url, { + body: JSON.stringify({ yaml }), + headers: { + 'content-type': 'application/json', + ...auth.headers, + }, + method: 'POST', + }); + if (!res.ok) { + await this.handleApiError(res, 'Failed to execute test'); + } + + return await parseJsonResponse(res, 'Failed to execute test'); + } catch (error) { + if (error instanceof TypeError && error.message === 'fetch failed') { + throw this.enhanceFetchError(error, url); + } + + throw error; + } + }, + + async stopLiveSession( + baseUrl: string, + auth: AuthContext, + sessionName: string, + ): Promise { + const url = `${baseUrl}/live/${sessionName}`; + try { + const res = await fetch(url, { + headers: { ...auth.headers }, + method: 'DELETE', + }); + if (!res.ok) { + await this.handleApiError(res, 'Failed to stop session'); + } + } catch (error) { + if (error instanceof TypeError && error.message === 'fetch failed') { + throw this.enhanceFetchError(error, url); + } + + throw error; + } + }, + + async getLiveSession( + baseUrl: string, + auth: AuthContext, + sessionName: string, + ): Promise { + const url = `${baseUrl}/live/${sessionName}`; + try { + const res = await fetch(url, { + headers: { ...auth.headers }, + method: 'GET', + }); + if (!res.ok) { + await this.handleApiError(res, 'Failed to get session status'); + } + + return await parseJsonResponse(res, 'Failed to get session status'); + } catch (error) { + if (error instanceof TypeError && error.message === 'fetch failed') { + throw this.enhanceFetchError(error, url); + } + + throw error; + } + }, }; From f0082fa8cb6877e8af1ecd594c5279f56162c73a Mon Sep 17 00:00:00 2001 From: Tom Riglar Date: Sat, 13 Jun 2026 08:54:21 +0100 Subject: [PATCH 2/2] refactor: move live session types to src/types/domain Addresses code review on PR #13: cross-layer domain types belong in src/types/domain/ per CLAUDE.md (alongside auth.types.ts and device.types.ts), and the non-obvious reason these are hand-defined (swagger has no /live routes for openapi-typescript to generate from) is now captured in a comment. Co-Authored-By: Claude Fable 5 --- src/gateways/api-gateway.ts | 26 +++++--------------------- src/types/domain/live.types.ts | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 21 deletions(-) create mode 100644 src/types/domain/live.types.ts diff --git a/src/gateways/api-gateway.ts b/src/gateways/api-gateway.ts index c3a9e99..028b0ed 100644 --- a/src/gateways/api-gateway.ts +++ b/src/gateways/api-gateway.ts @@ -6,6 +6,11 @@ import { pipeline } from 'node:stream/promises'; import { TAppMetadata } from '../types'; import type { AuthContext } from '../types/domain/auth.types'; +import type { + LiveExecResult, + LiveSession, + LiveSessionSummary, +} from '../types/domain/live.types'; import { paths } from '../types/generated/schema.types'; /** @@ -37,27 +42,6 @@ async function parseJsonResponse(res: Response, operation: string): Promise