@@ -21,26 +21,43 @@ type integrationResponse struct {
2121 UpdatedAt * string `json:"updated_at,omitempty"`
2222}
2323
24- type repoInfoResponse struct {
24+ // accessibleRepoResponse is returned by GET /integration/accessible-repos.
25+ // It reflects data fetched live from the GitHub API.
26+ type accessibleRepoResponse struct {
2527 FullName string `json:"full_name"`
2628 Owner string `json:"owner"`
2729 RepoName string `json:"repo_name"`
2830 DefaultBranch string `json:"default_branch"`
2931 Private bool `json:"private"`
3032}
3133
32- type linkedRepoResponse struct {
34+ // repositoryResponse is the canonical DTO for a project-linked repository.
35+ // Returned by GET /repositories and POST /repositories.
36+ type repositoryResponse struct {
3337 ID string `json:"id"`
3438 ProjectID string `json:"project_id"`
3539 Owner string `json:"owner"`
3640 RepoName string `json:"repo_name"`
3741 FullName string `json:"full_name"`
3842 DefaultBranch string `json:"default_branch"`
43+ CloneURL string `json:"clone_url"`
3944 WebhookActive bool `json:"webhook_active"`
4045 CreatedAt string `json:"created_at"`
4146 UpdatedAt string `json:"updated_at"`
4247}
4348
49+ // repoCloneInfo is returned by GET /repositories/:repoId/clone-info.
50+ // It includes a short-lived token for cloning.
51+ type repoCloneInfo struct {
52+ ID string `json:"id"`
53+ FullName string `json:"full_name"`
54+ Owner string `json:"owner"`
55+ RepoName string `json:"repo_name"`
56+ CloneURL string `json:"clone_url"`
57+ Token string `json:"token"`
58+ ExpiresAt float64 `json:"expires_at"`
59+ }
60+
4461const githubPluginID = "com.paca.github"
4562
4663func webhookURLFromPublicURL (cfg * plugin.Config , projectID string ) (string , error ) {
@@ -69,7 +86,7 @@ func (p *githubPlugin) decryptToken(projectID string) (string, error) {
6986 return p .decrypt (enc )
7087}
7188
72- // ─── GET /github ────── ────────────────────────────────────────────────────────
89+ // ─── GET /integration ────────────────────────────────────────────────────────
7390
7491func (p * githubPlugin ) getIntegration (req * plugin.Request , res * plugin.Response ) {
7592 projectID := req .Caller .ProjectID
@@ -96,7 +113,7 @@ func (p *githubPlugin) getIntegration(req *plugin.Request, res *plugin.Response)
96113 })
97114}
98115
99- // ─── POST /github /token ────── ─────────────────────────────────────────────────
116+ // ─── POST /integration /token ─────────────────────────────────────────────────
100117
101118func (p * githubPlugin ) setToken (req * plugin.Request , res * plugin.Response ) {
102119 projectID := req .Caller .ProjectID
@@ -129,12 +146,11 @@ func (p *githubPlugin) setToken(req *plugin.Request, res *plugin.Response) {
129146 }
130147
131148 now := nowStr ()
132- id := uuid .New ().String ()
133149 _ , err = p .db .Exec (`
134- INSERT INTO github_integrations (id, project_id, access_token_enc, created_at, updated_at)
135- VALUES ($1, $2, $3, $4, $4 )
136- ON CONFLICT (project_id) DO UPDATE SET access_token_enc = $3 , updated_at = $4
137- ` , id , projectID , enc , now )
150+ INSERT INTO github_integrations (project_id, access_token_enc, created_at, updated_at)
151+ VALUES ($1, $2, $3, $4)
152+ ON CONFLICT (project_id) DO UPDATE SET access_token_enc = EXCLUDED.access_token_enc , updated_at = EXCLUDED.updated_at
153+ ` , projectID , enc , now , now )
138154 if err != nil {
139155 apiError (res , 500 , "INTERNAL_ERROR" , err .Error ())
140156 return
@@ -160,7 +176,7 @@ func (p *githubPlugin) setToken(req *plugin.Request, res *plugin.Response) {
160176 })
161177}
162178
163- // ─── DELETE /github /token ────── ───────────────────────────────────────────────
179+ // ─── DELETE /integration /token ───────────────────────────────────────────────
164180
165181func (p * githubPlugin ) deleteToken (req * plugin.Request , res * plugin.Response ) {
166182 projectID := req .Caller .ProjectID
@@ -192,9 +208,9 @@ func (p *githubPlugin) deleteToken(req *plugin.Request, res *plugin.Response) {
192208 noContent (res )
193209}
194210
195- // ─── GET /github/repositories ────────── ──────────────────────────────────────
211+ // ─── GET /integration/accessible-repos ──────────────────────────────────────
196212
197- func (p * githubPlugin ) listRepositories (req * plugin.Request , res * plugin.Response ) {
213+ func (p * githubPlugin ) listAccessibleRepos (req * plugin.Request , res * plugin.Response ) {
198214 projectID := req .Caller .ProjectID
199215
200216 token , err := p .decryptToken (projectID )
@@ -210,9 +226,9 @@ func (p *githubPlugin) listRepositories(req *plugin.Request, res *plugin.Respons
210226 return
211227 }
212228
213- items := make ([]repoInfoResponse , len (repos ))
229+ items := make ([]accessibleRepoResponse , len (repos ))
214230 for i , r := range repos {
215- items [i ] = repoInfoResponse {
231+ items [i ] = accessibleRepoResponse {
216232 FullName : r .FullName ,
217233 Owner : r .Owner .Login ,
218234 RepoName : r .Name ,
@@ -223,9 +239,9 @@ func (p *githubPlugin) listRepositories(req *plugin.Request, res *plugin.Respons
223239 ok (res , items )
224240}
225241
226- // ─── GET /github/linked- repositories ─────────────────────────────────────────
242+ // ─── GET /repositories ────────────── ─────────────────────────────────────────
227243
228- func (p * githubPlugin ) listLinkedRepositories (req * plugin.Request , res * plugin.Response ) {
244+ func (p * githubPlugin ) listRepositories (req * plugin.Request , res * plugin.Response ) {
229245 projectID := req .Caller .ProjectID
230246
231247 result , err := p .db .Query (`
@@ -237,16 +253,18 @@ func (p *githubPlugin) listLinkedRepositories(req *plugin.Request, res *plugin.R
237253 return
238254 }
239255
240- items := make ([]linkedRepoResponse , 0 , len (result .Rows ))
256+ items := make ([]repositoryResponse , 0 , len (result .Rows ))
241257 for _ , row := range result .Rows {
242258 sc := newRowScanner (result .Columns , row )
243- items = append (items , linkedRepoResponse {
259+ fullName := sc .str ("full_name" )
260+ items = append (items , repositoryResponse {
244261 ID : sc .str ("id" ),
245262 ProjectID : sc .str ("project_id" ),
246263 Owner : sc .str ("owner" ),
247264 RepoName : sc .str ("repo_name" ),
248- FullName : sc . str ( "full_name" ) ,
265+ FullName : fullName ,
249266 DefaultBranch : sc .str ("default_branch" ),
267+ CloneURL : "https://github.com/" + fullName + ".git" ,
250268 WebhookActive : sc .int64Val ("webhook_id" ) > 0 ,
251269 CreatedAt : sc .str ("created_at" ),
252270 UpdatedAt : sc .str ("updated_at" ),
@@ -255,7 +273,7 @@ func (p *githubPlugin) listLinkedRepositories(req *plugin.Request, res *plugin.R
255273 ok (res , items )
256274}
257275
258- // ─── POST /github/linked- repositories ────────────────────────────────────────
276+ // ─── POST /repositories ─────────────── ────────────────────────────────────────
259277
260278func (p * githubPlugin ) linkRepository (req * plugin.Request , res * plugin.Response ) {
261279 projectID := req .Caller .ProjectID
@@ -352,20 +370,21 @@ func (p *githubPlugin) linkRepository(req *plugin.Request, res *plugin.Response)
352370 return
353371 }
354372
355- created (res , linkedRepoResponse {
373+ created (res , repositoryResponse {
356374 ID : repoID ,
357375 ProjectID : projectID ,
358376 Owner : ghRepo .Owner .Login ,
359377 RepoName : ghRepo .Name ,
360378 FullName : ghRepo .FullName ,
361379 DefaultBranch : ghRepo .DefaultBranch ,
380+ CloneURL : "https://github.com/" + ghRepo .FullName + ".git" ,
362381 WebhookActive : webhookID > 0 ,
363382 CreatedAt : now ,
364383 UpdatedAt : now ,
365384 })
366385}
367386
368- // ─── DELETE /github/linked- repositories/:repoId ───────────────────────────────
387+ // ─── DELETE /repositories/:repoId ───────────── ───────────────────────────────
369388
370389func (p * githubPlugin ) unlinkRepository (req * plugin.Request , res * plugin.Response ) {
371390 projectID := req .Caller .ProjectID
@@ -405,6 +424,48 @@ func (p *githubPlugin) unlinkRepository(req *plugin.Request, res *plugin.Respons
405424 noContent (res )
406425}
407426
427+ // ─── GET /repositories/:repoId/clone-info ────────────────────────────────────
428+
429+ func (p * githubPlugin ) getRepoCloneInfo (req * plugin.Request , res * plugin.Response ) {
430+ projectID := req .Caller .ProjectID
431+ repoID := req .PathParam ("repoId" )
432+ if repoID == "" {
433+ apiError (res , 400 , "BAD_REQUEST" , "repoId path parameter is required" )
434+ return
435+ }
436+ result , err := p .db .Query (
437+ `SELECT id, full_name, owner, repo_name FROM github_repositories WHERE project_id = $1 AND id = $2` ,
438+ projectID , repoID ,
439+ )
440+ if err != nil {
441+ apiError (res , 500 , "INTERNAL_ERROR" , err .Error ())
442+ return
443+ }
444+ if len (result .Rows ) == 0 {
445+ apiError (res , 404 , "GITHUB_REPOSITORY_NOT_FOUND" , "repository not found" )
446+ return
447+ }
448+ token , err := p .decryptToken (projectID )
449+ if err != nil {
450+ writeAppError (res , err )
451+ return
452+ }
453+ sc := newRowScanner (result .Columns , result .Rows [0 ])
454+ fullName := sc .str ("full_name" )
455+ ok (res , repoCloneInfo {
456+ ID : sc .str ("id" ),
457+ FullName : fullName ,
458+ Owner : sc .str ("owner" ),
459+ RepoName : sc .str ("repo_name" ),
460+ CloneURL : "https://github.com/" + fullName + ".git" ,
461+ Token : token ,
462+ ExpiresAt : 0 ,
463+ })
464+ }
465+
466+ // ─── GET /github/repo-info (REMOVED) ─────────────────────────────────────────
467+ // Replaced by GET /repositories (list all) and GET /repositories/:repoId/clone-info
468+
408469// ─── Error helpers ────────────────────────────────────────────────────────────
409470
410471type appError struct {
0 commit comments