1- import { usePipelineStore } from "../store/usePipelineStore" ;
1+ // In dev: talk to Vite dev server proxy at /api
2+ // In prod: use the real backend URL from VITE_API_BASE (e.g. https://api.autodeploy.app)
3+ const DEFAULT_API_BASE =
4+ import . meta. env . MODE === "development" ? "/api" : "" ;
25
36export const BASE =
4- import . meta. env . VITE_API_BASE ?? "http://localhost:3000/api" ;
7+ import . meta. env . VITE_API_BASE || DEFAULT_API_BASE ;
58
6- // Derive the server base without any trailing "/api" for MCP calls
7- const SERVER_BASE = BASE . replace ( / \/ a p i $ / , "" ) ;
9+ // SERVER_BASE is the same as BASE but without trailing /api,
10+ // so we can call /mcp and /auth directly.
11+ const SERVER_BASE = BASE . endsWith ( "/api" )
12+ ? BASE . slice ( 0 , - 4 )
13+ : BASE ;
814
915// Generic REST helper for /api/* endpoints
1016async function request < T > ( path : string , opts : RequestInit = { } ) : Promise < T > {
@@ -23,24 +29,73 @@ async function mcp<T>(
2329 tool : string ,
2430 input : Record < string , any > = { }
2531) : Promise < T > {
26- const res = await fetch (
27- `${ SERVER_BASE } /mcp/v1/${ encodeURIComponent ( tool ) } ` ,
28- {
29- method : "POST" ,
30- headers : { "Content-Type" : "application/json" } ,
31- credentials : "include" ,
32- body : JSON . stringify ( input ) ,
33- }
34- ) ;
32+ const url = `${ SERVER_BASE } /mcp/v1/${ encodeURIComponent ( tool ) } ` ;
33+ const res = await fetch ( url , {
34+ method : "POST" ,
35+ headers : { "Content-Type" : "application/json" } ,
36+ credentials : "include" ,
37+ body : JSON . stringify ( input ) ,
38+ } ) ;
3539 const payload = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
3640 if ( ! res . ok || ( payload as any ) ?. success === false ) {
3741 const msg = ( payload as any ) ?. error || res . statusText || "MCP error" ;
3842 throw new Error ( msg ) ;
3943 }
40- // payload = { success: true, data: {...} }
4144 return ( payload as any ) . data as T ;
4245}
4346
47+
48+ // A single saved YAML version from pipeline_history
49+ export type PipelineVersion = {
50+ id : string ;
51+ user_id : string ;
52+ repo_full_name : string ;
53+ branch : string ;
54+ workflow_path : string ;
55+ yaml : string ;
56+ yaml_hash : string ;
57+ source : string ;
58+ created_at : string ;
59+ } ;
60+
61+ // // Derive the server base without any trailing "/api" for MCP calls
62+ // const SERVER_BASE = BASE.replace(/\/api$/, "");
63+
64+ // // Generic REST helper for /api/* endpoints
65+ // async function request<T>(path: string, opts: RequestInit = {}): Promise<T> {
66+ // const res = await fetch(`${BASE}${path}`, {
67+ // headers: { "Content-Type": "application/json" },
68+ // credentials: "include",
69+ // ...opts,
70+ // });
71+ // const data = await res.json().catch(() => ({}));
72+ // if (!res.ok) throw new Error((data as any)?.error || res.statusText);
73+ // return data as T;
74+ // }
75+
76+ // // Helper for MCP tool calls on the server at /mcp/v1/:tool_name
77+ // async function mcp<T>(
78+ // tool: string,
79+ // input: Record<string, any> = {}
80+ // ): Promise<T> {
81+ // const res = await fetch(
82+ // `${SERVER_BASE}/mcp/v1/${encodeURIComponent(tool)}`,
83+ // {
84+ // method: "POST",
85+ // headers: { "Content-Type": "application/json" },
86+ // credentials: "include",
87+ // body: JSON.stringify(input),
88+ // }
89+ // );
90+ // const payload = await res.json().catch(() => ({}));
91+ // if (!res.ok || (payload as any)?.success === false) {
92+ // const msg = (payload as any)?.error || res.statusText || "MCP error";
93+ // throw new Error(msg);
94+ // }
95+ // // payload = { success: true, data: {...} }
96+ // return (payload as any).data as T;
97+ // }
98+
4499// Simple in-memory cache for AWS roles to avoid hammering MCP
45100let cachedAwsRoles : string [ ] | null = null ;
46101let awsRolesAttempted = false ;
@@ -51,6 +106,58 @@ const cachedBranches = new Map<string, string[]>();
51106
52107export const api = {
53108
109+ // ===== Pipeline history + rollback =====
110+
111+ async getPipelineHistory ( params : {
112+ repoFullName : string ;
113+ branch ?: string ;
114+ path ?: string ;
115+ limit ?: number ;
116+ } ) : Promise < PipelineVersion [ ] > {
117+ const { repoFullName, branch, path, limit } = params ;
118+
119+ const qs = new URLSearchParams ( ) ;
120+ qs . set ( "repoFullName" , repoFullName ) ;
121+ if ( branch ) qs . set ( "branch" , branch ) ;
122+ if ( path ) qs . set ( "path" , path ) ;
123+ if ( limit ) qs . set ( "limit" , String ( limit ) ) ;
124+
125+ const res = await fetch (
126+ `${ SERVER_BASE } /mcp/v1/pipeline_history?${ qs . toString ( ) } ` ,
127+ {
128+ method : "GET" ,
129+ credentials : "include" ,
130+ }
131+ ) ;
132+
133+ const payload = await res . json ( ) . catch ( ( ) => ( { } as any ) ) ;
134+ if ( ! res . ok || ! payload . ok ) {
135+ throw new Error ( payload . error || res . statusText || "History failed" ) ;
136+ }
137+
138+ // Back-end shape: { ok: true, versions: { rows: [...] } }
139+ const rows = ( payload . versions ?. rows ?? [ ] ) as PipelineVersion [ ] ;
140+ return rows ;
141+ } ,
142+
143+ async rollbackPipeline ( versionId : string ) : Promise < any > {
144+ const res = await fetch ( `${ SERVER_BASE } /mcp/v1/pipeline_rollback` , {
145+ method : "POST" ,
146+ headers : { "Content-Type" : "application/json" } ,
147+ credentials : "include" ,
148+ body : JSON . stringify ( { versionId } ) ,
149+ } ) ;
150+
151+ const payload = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
152+ if ( ! res . ok || ! payload . ok ) {
153+ throw new Error ( payload . error || res . statusText || "Rollback failed" ) ;
154+ }
155+
156+ // Backend mention: data.data.github.commit.html_url, etc
157+ return payload . data ;
158+ } ,
159+
160+
54161 async listAwsRoles ( ) : Promise < { roles : string [ ] } > {
55162 // If we've already successfully fetched roles, reuse them.
56163 if ( cachedAwsRoles && cachedAwsRoles . length > 0 ) {
@@ -181,10 +288,91 @@ export const api = {
181288 return { branches : cachedBranches . get ( repo ) ?? [ ] } ;
182289 }
183290 } ,
291+ // async listRepos(): Promise<{ repos: string[] }> {
292+ // // If we already have repos cached, reuse them.
293+ // if (cachedRepos && cachedRepos.length > 0) {
294+ // return { repos: cachedRepos };
295+ // }
296+
297+ // // If we've already tried once and failed, don't hammer the server.
298+ // if (reposAttempted && !cachedRepos) {
299+ // return { repos: [] };
300+ // }
301+
302+ // reposAttempted = true;
303+
304+ // try {
305+ // const outer = await mcp<{
306+ // provider: string;
307+ // user: string;
308+ // repositories: { full_name: string }[];
309+ // }>("repo_reader", {});
310+
311+ // const repos = outer?.repositories?.map((r) => r.full_name) ?? [];
312+ // cachedRepos = repos;
313+ // return { repos };
314+ // } catch (err) {
315+ // console.error("[api.listRepos] failed:", err);
316+ // // Don't throw to avoid retry loops from effects; just return whatever we have (or empty).
317+ // return { repos: cachedRepos ?? [] };
318+ // }
319+ // },
320+
321+ // async listBranches(repo: string): Promise<{ branches: string[] }> {
322+ // // ✅ If we already have branches cached for this repo, reuse them.
323+ // const cached = cachedBranches.get(repo);
324+ // if (cached) {
325+ // return { branches: cached };
326+ // }
327+
328+ // try {
329+ // // For now we still use repo_reader, but we only call it
330+ // // when the cache is cold. We can later swap this to a
331+ // // more specific MCP tool like "repo_branches".
332+ // const outer = await mcp<{
333+ // success?: boolean;
334+ // data?: { repositories: { full_name: string; branches?: string[] }[] };
335+ // repositories?: { full_name: string; branches?: string[] }[];
336+ // }>("repo_reader", {
337+ // // This extra input is safe: current server ignores it,
338+ // // future server can use it to optimize.
339+ // repoFullName: repo,
340+ // });
341+
342+ // // Unwrap the payload (tool responses come back as { success, data })
343+ // const body = (outer as any)?.data ?? outer;
344+
345+ // const match = body?.repositories?.find((r: any) => r.full_name === repo);
346+ // const branches = match?.branches ?? [];
347+
348+ // // Cache even empty arrays so we don't re-query a repo with no branches
349+ // cachedBranches.set(repo, branches);
350+
351+ // return { branches };
352+ // } catch (err) {
353+ // console.error("[api.listBranches] failed:", err);
354+
355+ // // If we have anything cached (even empty), use it.
356+ // const fallback = cachedBranches.get(repo) ?? [];
357+ // return { branches: fallback };
358+ // }
359+ // },
184360
185361 async createPipeline ( payload : any ) {
186- const { repo, branch, template = "node_app" , options } = payload || { } ;
187- const data = await mcp ( "pipeline_generator" , payload ) ;
362+ const {
363+ repo,
364+ branch,
365+ template = "node_app" ,
366+ provider = "aws" ,
367+ options,
368+ } = payload || { } ;
369+ const data = await mcp ( "pipeline_generator" , {
370+ repo,
371+ branch,
372+ provider,
373+ template,
374+ options : options || { } ,
375+ } ) ;
188376 return data ;
189377 } ,
190378
0 commit comments