Skip to content

Commit 99a9aff

Browse files
authored
Application resource connection, save git username (#34)
1 parent ac2ae7f commit 99a9aff

File tree

8 files changed

+318
-18
lines changed

8 files changed

+318
-18
lines changed

clients/api/client.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,38 @@ func (c *Client) GetVersionStatus(applicationID, organizationID, versionID strin
299299
return &resp, nil
300300
}
301301

302+
// --- Resource endpoints ---
303+
304+
// GetResources retrieves all resources for an organization
305+
func (c *Client) GetResources(organizationID string) (*GetResourcesResponse, error) {
306+
req := GetResourcesRequest{
307+
OrganizationID: organizationID,
308+
}
309+
310+
var resp GetResourcesResponse
311+
err := c.doRequest("POST", "/resources", req, &resp)
312+
if err != nil {
313+
return nil, err
314+
}
315+
return &resp, nil
316+
}
317+
318+
// SaveApplicationResources saves the selected resources for an application
319+
func (c *Client) SaveApplicationResources(organizationID, applicationID string, resourceIDs []string) (*SaveApplicationResourcesResponse, error) {
320+
req := SaveApplicationResourcesRequest{
321+
OrganizationID: organizationID,
322+
ApplicationID: applicationID,
323+
ResourceIDs: resourceIDs,
324+
}
325+
326+
var resp SaveApplicationResourcesResponse
327+
err := c.doRequest("POST", "/application-resources", req, &resp)
328+
if err != nil {
329+
return nil, err
330+
}
331+
return &resp, nil
332+
}
333+
302334
// --- Version Check endpoints ---
303335

304336
// CheckVersion checks if the CLI version is up to date

clients/api/structs.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,32 @@ type GetVersionStatusResponse struct {
161161
DeploymentError string `json:"deploymentError,omitempty"`
162162
}
163163

164+
// --- Resource structs ---
165+
166+
// GetResourcesRequest represents the request body for POST /resources
167+
type GetResourcesRequest struct {
168+
OrganizationID string `json:"organizationId"`
169+
}
170+
171+
// GetResourcesResponse represents the response from POST /resources
172+
type GetResourcesResponse struct {
173+
Error *AppErrorDetail `json:"error,omitempty"`
174+
Resources []ResourceItem `json:"resources,omitempty"`
175+
}
176+
177+
// SaveApplicationResourcesRequest represents the request body for POST /application-resources
178+
type SaveApplicationResourcesRequest struct {
179+
OrganizationID string `json:"organizationId"`
180+
ApplicationID string `json:"applicationId"`
181+
ResourceIDs []string `json:"resourceIds"`
182+
}
183+
184+
// SaveApplicationResourcesResponse represents the response from POST /application-resources
185+
type SaveApplicationResourcesResponse struct {
186+
Error *AppErrorDetail `json:"error,omitempty"`
187+
Success bool `json:"success,omitempty"`
188+
}
189+
164190
// --- Version Check structs ---
165191

166192
// CheckVersionResponse represents the response from GET /version/check

clients/token/token.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const (
1515
keyringOrgUser = "default-org"
1616
// keyringOrgName is the name for storing the default organization name in the system keyring
1717
keyringOrgName = "default-org-name"
18+
// keyringGithubUsername is the key for storing the GitHub username in the system keyring
19+
keyringGithubUsername = "github-username"
1820
)
1921

2022
// storeToken saves the access token to the system keyring
@@ -82,3 +84,39 @@ func DeleteDefaultOrg() error {
8284
}
8385
return nil
8486
}
87+
88+
// StoreGithubUsername saves the GitHub username to the system keyring
89+
func StoreGithubUsername(username string) error {
90+
err := keyring.Set(keyringService, keyringGithubUsername, username)
91+
if err != nil {
92+
return fmt.Errorf("failed to store GitHub username in keyring: %w", err)
93+
}
94+
return nil
95+
}
96+
97+
// GetGithubUsername retrieves the GitHub username from the system keyring
98+
// Returns empty string and nil error if not found
99+
func GetGithubUsername() (string, error) {
100+
username, err := keyring.Get(keyringService, keyringGithubUsername)
101+
if err != nil {
102+
// Check if it's a "not found" error
103+
if err == keyring.ErrNotFound {
104+
return "", nil
105+
}
106+
return "", fmt.Errorf("failed to get GitHub username from keyring: %w", err)
107+
}
108+
return username, nil
109+
}
110+
111+
// DeleteGithubUsername removes the GitHub username from the system keyring
112+
func DeleteGithubUsername() error {
113+
err := keyring.Delete(keyringService, keyringGithubUsername)
114+
if err != nil {
115+
// Ignore "not found" errors
116+
if err == keyring.ErrNotFound {
117+
return nil
118+
}
119+
return fmt.Errorf("failed to delete GitHub username from keyring: %w", err)
120+
}
121+
return nil
122+
}

cmd/app/create.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"path/filepath"
77

8+
"github.com/charmbracelet/bubbles/key"
89
"github.com/charmbracelet/huh"
910
"github.com/charmbracelet/lipgloss"
1011
"github.com/major-technology/cli/clients/api"
@@ -134,6 +135,13 @@ func runCreate(cobraCmd *cobra.Command) error {
134135
return fmt.Errorf("failed to ensure repository access: %w", err)
135136
}
136137

138+
// Select resources for the application
139+
cobraCmd.Println("\nSelecting resources for your application...")
140+
if err := selectApplicationResources(cobraCmd, orgID, createResp.ApplicationID); err != nil {
141+
errorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("9")) // Red
142+
cobraCmd.Println(errorStyle.Render("Failed to configure resources. Please run 'major app resources' to configure them later."))
143+
}
144+
137145
// Push to the new remote
138146
cobraCmd.Println("\nPushing to new repository...")
139147
if err := git.Push(tempDir); err != nil {
@@ -242,3 +250,80 @@ func printSuccessMessage(cobraCmd *cobra.Command, appName string) {
242250
cobraCmd.Println(cdInstruction)
243251
cobraCmd.Println(box)
244252
}
253+
254+
// selectApplicationResources prompts the user to select resources for the application
255+
func selectApplicationResources(cobraCmd *cobra.Command, orgID, appID string) error {
256+
// Get the API client
257+
apiClient := singletons.GetAPIClient()
258+
259+
// Fetch available resources
260+
resourcesResp, err := apiClient.GetResources(orgID)
261+
if ok := api.CheckErr(cobraCmd, err); !ok {
262+
return err
263+
}
264+
265+
// Check if there are any resources available
266+
if len(resourcesResp.Resources) == 0 {
267+
cobraCmd.Println("No resources available in this organization.")
268+
return nil
269+
}
270+
271+
// Create options for the multiselect
272+
options := make([]huh.Option[string], len(resourcesResp.Resources))
273+
for i, resource := range resourcesResp.Resources {
274+
// Format: "Name - Description"
275+
label := resource.Name
276+
if resource.Description != "" {
277+
label = fmt.Sprintf("%s - %s", resource.Name, resource.Description)
278+
}
279+
options[i] = huh.NewOption(label, resource.ID)
280+
}
281+
282+
// Create custom keymap where 'n' submits instead of enter
283+
customKeyMap := huh.NewDefaultKeyMap()
284+
customKeyMap.MultiSelect.Toggle = key.NewBinding(
285+
key.WithKeys(" ", "enter"),
286+
key.WithHelp("space/enter", "toggle"),
287+
)
288+
customKeyMap.MultiSelect.Submit = key.NewBinding(
289+
key.WithKeys("n"),
290+
key.WithHelp("n", "continue"),
291+
)
292+
// Disable the default next/prev behavior on enter
293+
customKeyMap.MultiSelect.Next = key.NewBinding(
294+
key.WithKeys("tab"),
295+
key.WithHelp("tab", "next field"),
296+
)
297+
298+
// Prompt user to select resources
299+
var selectedResourceIDs []string
300+
form := huh.NewForm(
301+
huh.NewGroup(
302+
huh.NewMultiSelect[string]().
303+
Title("Select resources for your application").
304+
Description("Use space/enter to select, 'n' to continue").
305+
Options(options...).
306+
Value(&selectedResourceIDs),
307+
),
308+
).WithKeyMap(customKeyMap)
309+
310+
if err := form.Run(); err != nil {
311+
return fmt.Errorf("failed to collect resource selection: %w", err)
312+
}
313+
314+
// If no resources selected, just return
315+
if len(selectedResourceIDs) == 0 {
316+
cobraCmd.Println("No resources selected.")
317+
return nil
318+
}
319+
320+
// Save the selected resources
321+
cobraCmd.Printf("Saving %d selected resource(s)...\n", len(selectedResourceIDs))
322+
_, err = apiClient.SaveApplicationResources(orgID, appID, selectedResourceIDs)
323+
if ok := api.CheckErr(cobraCmd, err); !ok {
324+
return err
325+
}
326+
327+
cobraCmd.Printf("✓ Resources configured successfully\n")
328+
return nil
329+
}

cmd/app/helper.go

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/charmbracelet/huh"
1111
"github.com/major-technology/cli/clients/git"
12+
mjrToken "github.com/major-technology/cli/clients/token"
1213
"github.com/major-technology/cli/singletons"
1314
"github.com/major-technology/cli/utils"
1415
"github.com/spf13/cobra"
@@ -163,24 +164,60 @@ func testGitAccess(repoURL string) bool {
163164
// ensureRepositoryAccess ensures the user has access to the repository by inviting them as a collaborator
164165
// This function prompts for GitHub username, sends an invite, and waits for access to be granted
165166
func ensureRepositoryAccess(cmd *cobra.Command, appID string, sshURL string, httpsURL string) error {
166-
// Prompt for GitHub username
167+
// Check if GitHub username is stored in keychain
168+
storedUsername, err := mjrToken.GetGithubUsername()
169+
if err != nil {
170+
return fmt.Errorf("failed to check stored GitHub username: %w", err)
171+
}
172+
167173
var githubUsername string
168-
form := huh.NewForm(
169-
huh.NewGroup(
170-
huh.NewInput().
171-
Title("What is your GitHub username?").
172-
Value(&githubUsername).
173-
Validate(func(s string) error {
174-
if s == "" {
175-
return fmt.Errorf("GitHub username is required")
176-
}
177-
return nil
178-
}),
179-
),
180-
)
181-
182-
if err := form.Run(); err != nil {
183-
return fmt.Errorf("failed to get GitHub username: %w", err)
174+
175+
// If we have a stored username, confirm with the user
176+
if storedUsername != "" {
177+
var useStored bool
178+
confirmForm := huh.NewForm(
179+
huh.NewGroup(
180+
huh.NewConfirm().
181+
Title(fmt.Sprintf("Use GitHub username: %s?", storedUsername)).
182+
Description("We have your GitHub username saved. Would you like to use it?").
183+
Value(&useStored),
184+
),
185+
)
186+
187+
if err := confirmForm.Run(); err != nil {
188+
return fmt.Errorf("failed to confirm GitHub username: %w", err)
189+
}
190+
191+
if useStored {
192+
githubUsername = storedUsername
193+
}
194+
}
195+
196+
// If no stored username or user declined to use it, prompt for username
197+
if githubUsername == "" {
198+
form := huh.NewForm(
199+
huh.NewGroup(
200+
huh.NewInput().
201+
Title("What is your GitHub username?").
202+
Value(&githubUsername).
203+
Validate(func(s string) error {
204+
if s == "" {
205+
return fmt.Errorf("GitHub username is required")
206+
}
207+
return nil
208+
}),
209+
),
210+
)
211+
212+
if err := form.Run(); err != nil {
213+
return fmt.Errorf("failed to get GitHub username: %w", err)
214+
}
215+
216+
// Store the username for future use
217+
if err := mjrToken.StoreGithubUsername(githubUsername); err != nil {
218+
// Log the error but don't fail the operation
219+
cmd.Printf("Warning: Failed to save GitHub username: %v\n", err)
220+
}
184221
}
185222

186223
cmd.Printf("\nAdding @%s as a collaborator to the repository...\n", githubUsername)
@@ -192,7 +229,7 @@ func ensureRepositoryAccess(cmd *cobra.Command, appID string, sshURL string, htt
192229
}
193230

194231
// Add user as GitHub collaborator
195-
_, err := apiClient.AddGithubCollaborators(appID, githubUsername)
232+
_, err = apiClient.AddGithubCollaborators(appID, githubUsername)
196233
if err != nil {
197234
return fmt.Errorf("failed to add GitHub collaborator: %w", err)
198235
}

cmd/git/config.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package git
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/charmbracelet/huh"
7+
mjrToken "github.com/major-technology/cli/clients/token"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
// configCmd represents the git config command
12+
var configCmd = &cobra.Command{
13+
Use: "config",
14+
Short: "Configure git settings",
15+
Long: `Configure git-related settings such as your GitHub username.`,
16+
Run: func(cobraCmd *cobra.Command, args []string) {
17+
cobra.CheckErr(runConfig(cobraCmd))
18+
},
19+
}
20+
21+
func runConfig(cobraCmd *cobra.Command) error {
22+
// Get current GitHub username if it exists
23+
currentUsername, err := mjrToken.GetGithubUsername()
24+
if err != nil {
25+
return fmt.Errorf("failed to get current GitHub username: %w", err)
26+
}
27+
28+
// Show current username if it exists
29+
if currentUsername != "" {
30+
cobraCmd.Printf("Current GitHub username: %s\n\n", currentUsername)
31+
}
32+
33+
// Prompt for new GitHub username
34+
var githubUsername string
35+
form := huh.NewForm(
36+
huh.NewGroup(
37+
huh.NewInput().
38+
Title("GitHub Username").
39+
Description("Enter your GitHub username").
40+
Value(&githubUsername).
41+
Placeholder(currentUsername).
42+
Validate(func(s string) error {
43+
if s == "" {
44+
return fmt.Errorf("GitHub username is required")
45+
}
46+
return nil
47+
}),
48+
),
49+
)
50+
51+
if err := form.Run(); err != nil {
52+
return fmt.Errorf("failed to collect GitHub username: %w", err)
53+
}
54+
55+
// Store the GitHub username
56+
if err := mjrToken.StoreGithubUsername(githubUsername); err != nil {
57+
return fmt.Errorf("failed to store GitHub username: %w", err)
58+
}
59+
60+
cobraCmd.Printf("✓ GitHub username saved: %s\n", githubUsername)
61+
return nil
62+
}
63+

0 commit comments

Comments
 (0)