1- const BASE = import . meta. env . VITE_API_BASE ?? "http://localhost:3333/api" ;
1+ export const BASE =
2+ import . meta. env . VITE_API_BASE ?? "http://localhost:3333/api" ;
3+
4+ // Derive the server base without any trailing "/api" for MCP calls
5+ const SERVER_BASE = BASE . replace ( / \/ a p i $ / , "" ) ;
26
37async function request < T > ( path : string , opts : RequestInit = { } ) : Promise < T > {
48 const res = await fetch ( `${ BASE } ${ path } ` , {
@@ -7,26 +11,197 @@ async function request<T>(path: string, opts: RequestInit = {}): Promise<T> {
711 ...opts ,
812 } ) ;
913 const data = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
10- if ( ! res . ok ) throw new Error ( data ?. error || res . statusText ) ;
14+ if ( ! res . ok ) throw new Error ( ( data as any ) ?. error || res . statusText ) ;
1115 return data as T ;
1216}
1317
18+ // Helper for MCP tool calls on the server at /mcp/v1/:tool_name
19+ async function mcp < T > ( tool : string , input : Record < string , any > = { } ) : Promise < T > {
20+ const res = await fetch ( `${ SERVER_BASE } /mcp/v1/${ encodeURIComponent ( tool ) } ` , {
21+ method : "POST" ,
22+ headers : { "Content-Type" : "application/json" } ,
23+ credentials : "include" ,
24+ body : JSON . stringify ( input ) ,
25+ } ) ;
26+ const payload = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
27+ if ( ! res . ok || payload ?. success === false ) {
28+ const msg = payload ?. error || res . statusText || "MCP error" ;
29+ throw new Error ( msg ) ;
30+ }
31+ return payload . data as T ;
32+ }
33+
1434export const api = {
15- listRepos : ( ) => request < { repos : string [ ] } > ( "/mcp/repos" ) ,
16- listBranches : ( repo : string ) => request < { branches : string [ ] } > ( `/mcp/repos/${ encodeURIComponent ( repo ) } /branches` ) ,
17- createPipeline : ( payload : any ) => request ( "/mcp/pipeline" , { method : "POST" , body : JSON . stringify ( payload ) } ) ,
18- listAwsRoles : ( ) => request < { roles : string [ ] } > ( "/mcp/oidc/roles" ) ,
19- openPr : ( payload : any ) => request ( "/mcp/pull-request" , { method : "POST" , body : JSON . stringify ( payload ) } ) ,
20- getConnections : ( repo : string ) => request ( `/mcp/config/connections?repo=${ encodeURIComponent ( repo ) } ` ) ,
21- getSecretPresence : ( repo : string , env : string ) => request ( `/mcp/config/secrets?repo=${ encodeURIComponent ( repo ) } &env=${ env } ` ) ,
22- setSecret : ( body : any ) => request ( "/mcp/config/secret" , { method : "POST" , body : JSON . stringify ( body ) } ) ,
23- runPreflight : ( body : any ) => request ( "/mcp/config/preflight" , { method : "POST" , body : JSON . stringify ( body ) } ) ,
24- startDeploy : ( body : any ) => request ( "/mcp/deploy/start" , { method : "POST" , body : JSON . stringify ( body ) } ) ,
25- streamJob ( jobId : string , onEvent : ( e : any ) => void ) {
26- const es = new EventSource ( `${ BASE } /mcp/jobs/${ jobId } /events` , { withCredentials : true } ) ;
27- es . onmessage = ( evt ) => onEvent ( JSON . parse ( evt . data ) ) ;
28- es . onerror = ( ) => es . close ( ) ;
29- return ( ) => es . close ( ) ;
35+ // Pull repos via MCP repo_reader tool (mocked backend). Maps to simple string[] of full_name.
36+ async listRepos ( ) : Promise < { repos : string [ ] } > {
37+ const data = await mcp < {
38+ repositories : { name : string ; full_name : string ; branches ?: string [ ] } [ ] ;
39+ } > ( "repo_reader" , { } ) ;
40+ const repos = ( data ?. repositories ?? [ ] ) . map ( ( r ) => r . full_name ) ;
41+ return { repos } ;
42+ } ,
43+
44+ // Derive branches by calling repo_reader again and selecting the repo.
45+ async listBranches ( repo : string ) : Promise < { branches : string [ ] } > {
46+ const data = await mcp < {
47+ repositories : { name : string ; full_name : string ; branches ?: string [ ] } [ ] ;
48+ } > ( "repo_reader" , { } ) ;
49+ const item = ( data ?. repositories ?? [ ] ) . find ( ( r ) => r . full_name === repo ) ;
50+ return { branches : item ?. branches ?? [ ] } ;
51+ } ,
52+
53+ // Generate pipeline via MCP pipeline_generator (mock). Assume provider 'aws' for now.
54+ async createPipeline ( payload : any ) {
55+ const { repo, branch, template = "node_app" , options } = payload || { } ;
56+ const data = await mcp ( "pipeline_generator" , {
57+ repo,
58+ branch,
59+ provider : "aws" ,
60+ template,
61+ options : options || { } ,
62+ } ) ;
63+ return data ;
64+ } ,
65+
66+ // List AWS roles via MCP oidc_adapter and map to list of ARNs.
67+ async listAwsRoles ( ) : Promise < { roles : string [ ] } > {
68+ const data = await mcp < { roles ?: { name : string ; arn : string } [ ] } > (
69+ "oidc_adapter" ,
70+ { provider : "aws" }
71+ ) ;
72+ const roles = ( data . roles ?? [ ] ) . map ( ( r ) => r . arn ) ;
73+ return { roles } ;
74+ } ,
75+
76+ // Not implemented on server yet; keep API shape but throw a helpful error.
77+ async openPr ( _payload : any ) {
78+ throw new Error ( "openPr is not implemented on the server (no MCP tool)" ) ;
79+ } ,
80+
81+ // --- Mocked config/secrets endpoints for Secrets/Preflight flow ---
82+ async getConnections ( _repo : string ) : Promise < {
83+ githubAppInstalled : boolean ;
84+ githubRepoWriteOk : boolean ;
85+ awsOidc : { connected : boolean ; roleArn ?: string ; accountId ?: string ; region ?: string } ;
86+ } > {
87+ // Try to fetch roles to populate a default role ARN
88+ let roleArn : string | undefined ;
89+ try {
90+ const { roles } = await this . listAwsRoles ( ) ;
91+ roleArn = roles [ 0 ] ;
92+ } catch { }
93+ return {
94+ githubAppInstalled : true ,
95+ githubRepoWriteOk : true ,
96+ awsOidc : { connected : ! ! roleArn , roleArn, accountId : "123456789012" , region : "us-east-1" } ,
97+ } ;
98+ } ,
99+
100+ async getSecretPresence ( repo : string , env : string ) : Promise < { key : string ; present : boolean } [ ] > {
101+ const required = [ "GITHUB_TOKEN" , "AWS_ROLE_ARN" ] ;
102+ const store = readSecrets ( repo , env ) ;
103+ return required . map ( ( k ) => ( { key : k , present : ! ! store [ k ] } ) ) ;
104+ } ,
105+
106+ async setSecret ( { repo, env, key, value } : { repo : string ; env : string ; key : string ; value : string } ) {
107+ const store = readSecrets ( repo , env ) ;
108+ store [ key ] = value ;
109+ writeSecrets ( repo , env , store ) ;
110+ return { ok : true } as const ;
111+ } ,
112+
113+ async runPreflight ( {
114+ repo,
115+ env,
116+ aws,
117+ } : {
118+ repo : string ;
119+ env : string ;
120+ aws ?: { roleArn ?: string ; region ?: string } ;
121+ } ) : Promise < { results : { label : string ; ok : boolean ; info ?: string } [ ] } > {
122+ const connections = await this . getConnections ( repo ) ;
123+ const secrets = await this . getSecretPresence ( repo , env ) ;
124+ const hasGithubApp = connections . githubAppInstalled ;
125+ const hasRepoWrite = connections . githubRepoWriteOk ;
126+ const role = aws ?. roleArn || connections . awsOidc . roleArn ;
127+ const hasAws = ! ! role ;
128+ const region = aws ?. region || connections . awsOidc . region || "us-east-1" ;
129+ const s = Object . fromEntries ( secrets . map ( ( x ) => [ x . key , x . present ] as const ) ) ;
130+
131+ const results = [
132+ { label : "GitHub App installed" , ok : hasGithubApp } ,
133+ { label : "Repo write access" , ok : hasRepoWrite } ,
134+ { label : "AWS OIDC configured" , ok : hasAws , info : role } ,
135+ { label : "Secret: GITHUB_TOKEN" , ok : ! ! s . GITHUB_TOKEN } ,
136+ { label : "Secret: AWS_ROLE_ARN" , ok : ! ! s . AWS_ROLE_ARN , info : role } ,
137+ { label : "AWS Region selected" , ok : ! ! region , info : region } ,
138+ ] ;
139+ return { results } ;
140+ } ,
141+
142+ // --- Mock deploy APIs for Dashboard ---
143+ async startDeploy ( { repo, env } : { repo : string ; env : string } ) {
144+ const jobId = `job_${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` ;
145+ // Stash minimal job info in memory for the stream to reference
146+ JOBS . set ( jobId , { repo, env, startedAt : Date . now ( ) } ) ;
147+ return { jobId } as const ;
148+ } ,
149+
150+ streamJob (
151+ jobId : string ,
152+ onEvent : ( e : { ts : string ; level : "info" | "warn" | "error" ; msg : string } ) => void
153+ ) {
154+ const meta = JOBS . get ( jobId ) || { repo : "?" , env : "dev" } ;
155+ const steps = [
156+ `Authenticating to AWS (${ meta . env } )` ,
157+ `Assuming role` ,
158+ `Validating permissions` ,
159+ `Building artifacts` ,
160+ `Deploying ${ meta . repo } ` ,
161+ `Verifying rollout` ,
162+ `Done`
163+ ] ;
164+ let i = 0 ;
165+ const timer = setInterval ( ( ) => {
166+ if ( i >= steps . length ) return ;
167+ const level = i === steps . length - 1 ? "info" : "info" ;
168+ onEvent ( { ts : new Date ( ) . toISOString ( ) , level, msg : steps [ i ++ ] } ) ;
169+ if ( i >= steps . length ) clearInterval ( timer ) ;
170+ } , 800 ) ;
171+ return ( ) => clearInterval ( timer ) ;
30172 } ,
31173} ;
32174
175+ // Helper to start GitHub OAuth (server redirects back after callback)
176+ export function startGitHubOAuth (
177+ redirectTo : string = window . location . origin
178+ ) {
179+ // Our server mounts OAuth at /auth/github/start and expects `redirect_to`
180+ const serverBase = BASE . replace ( / \/ a p i $ / , "" ) ;
181+ const url = `${ serverBase } /auth/github/start?redirect_to=${ encodeURIComponent (
182+ redirectTo
183+ ) } `;
184+ window . location . href = url ;
185+ }
186+
187+ // --- Local storage helpers for mock secrets ---
188+ function secKey ( repo : string , env : string ) {
189+ return `secrets:${ repo } :${ env } ` ;
190+ }
191+ function readSecrets ( repo : string , env : string ) : Record < string , string > {
192+ try {
193+ const raw = localStorage . getItem ( secKey ( repo , env ) ) ;
194+ return raw ? JSON . parse ( raw ) : { } ;
195+ } catch {
196+ return { } ;
197+ }
198+ }
199+ function writeSecrets ( repo : string , env : string , obj : Record < string , string > ) {
200+ try {
201+ localStorage . setItem ( secKey ( repo , env ) , JSON . stringify ( obj ) ) ;
202+ } catch { }
203+ }
204+
205+ // in-memory job storage for mock deploys
206+ const JOBS : Map < string , any > = new Map ( ) ;
207+
0 commit comments