Skip to content

Commit 796fb6a

Browse files
authored
Create flow asks for template, then generates resources if vite template (#35)
1 parent 99a9aff commit 796fb6a

File tree

4 files changed

+213
-22
lines changed

4 files changed

+213
-22
lines changed

clients/api/client.go

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

302+
// --- Template endpoints ---
303+
304+
// GetTemplates retrieves all available templates
305+
func (c *Client) GetTemplates() (*GetTemplatesResponse, error) {
306+
var resp GetTemplatesResponse
307+
err := c.doRequest("GET", "/templates", nil, &resp)
308+
if err != nil {
309+
return nil, err
310+
}
311+
return &resp, nil
312+
}
313+
302314
// --- Resource endpoints ---
303315

304316
// GetResources retrieves all resources for an organization

clients/api/structs.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ type ResourceItem struct {
9595
ID string `json:"id"`
9696
Name string `json:"name"`
9797
Description string `json:"description"`
98+
Type string `json:"type"`
9899
}
99100

100101
// GetApplicationResourcesResponse represents the response from GET /applications/:applicationId/resources
@@ -161,6 +162,21 @@ type GetVersionStatusResponse struct {
161162
DeploymentError string `json:"deploymentError,omitempty"`
162163
}
163164

165+
// --- Template structs ---
166+
167+
// TemplateItem represents a single template
168+
type TemplateItem struct {
169+
ID string `json:"id"`
170+
Name string `json:"name"`
171+
TemplateURL string `json:"templateUrl"`
172+
}
173+
174+
// GetTemplatesResponse represents the response from GET /templates
175+
type GetTemplatesResponse struct {
176+
Error *AppErrorDetail `json:"error,omitempty"`
177+
Templates []TemplateItem `json:"templates,omitempty"`
178+
}
179+
164180
// --- Resource structs ---
165181

166182
// GetResourcesRequest represents the request body for POST /resources

cmd/app/app.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,58 @@
11
package app
22

33
import (
4+
"fmt"
5+
"os/exec"
6+
7+
"github.com/charmbracelet/lipgloss"
48
"github.com/major-technology/cli/utils"
59
"github.com/spf13/cobra"
610
)
711

12+
// checkPnpmInstalled checks if pnpm is installed and provides installation instructions if not
13+
func checkPnpmInstalled(cmd *cobra.Command) error {
14+
_, err := exec.LookPath("pnpm")
15+
if err != nil {
16+
errorStyle := lipgloss.NewStyle().
17+
Bold(true).
18+
Foreground(lipgloss.Color("#FF5555"))
19+
20+
commandStyle := lipgloss.NewStyle().
21+
Bold(true).
22+
Foreground(lipgloss.Color("#87D7FF"))
23+
24+
boxStyle := lipgloss.NewStyle().
25+
Border(lipgloss.RoundedBorder()).
26+
BorderForeground(lipgloss.Color("#FF5555")).
27+
Padding(1, 2).
28+
MarginTop(1).
29+
MarginBottom(1)
30+
31+
message := fmt.Sprintf("%s\n\n%s\n %s\n\n%s\n %s",
32+
errorStyle.Render("❌ pnpm is required for app commands"),
33+
"Install pnpm using one of these methods:",
34+
commandStyle.Render("brew install pnpm"),
35+
"Or if you have Node.js installed:",
36+
commandStyle.Render("corepack enable"))
37+
38+
cmd.Println(boxStyle.Render(message))
39+
return fmt.Errorf("pnpm not found in PATH")
40+
}
41+
return nil
42+
}
43+
844
// Cmd represents the app command
945
var Cmd = &cobra.Command{
1046
Use: "app",
1147
Short: "Application management commands",
1248
Long: `Commands for creating and managing applications.`,
1349
Args: utils.NoArgs,
50+
PersistentPreRun: func(cmd *cobra.Command, args []string) {
51+
// Check if pnpm is installed before running any app command
52+
if err := checkPnpmInstalled(cmd); err != nil {
53+
cobra.CheckErr(err)
54+
}
55+
},
1456
Run: func(cmd *cobra.Command, args []string) {
1557
cmd.Help()
1658
},

cmd/app/create.go

Lines changed: 143 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package app
33
import (
44
"fmt"
55
"os"
6+
"os/exec"
67
"path/filepath"
78

89
"github.com/charmbracelet/bubbles/key"
@@ -15,8 +16,6 @@ import (
1516
"github.com/spf13/cobra"
1617
)
1718

18-
const templateRepoURL = "https://github.com/major-technology/basic-template.git"
19-
2019
// createCmd represents the create command
2120
var createCmd = &cobra.Command{
2221
Use: "create",
@@ -68,11 +67,18 @@ func runCreate(cobraCmd *cobra.Command) error {
6867
return fmt.Errorf("failed to collect application details: %w", err)
6968
}
7069

71-
cobraCmd.Printf("\nCreating application '%s'...\n", appName)
72-
7370
// Get the API client
7471
apiClient := singletons.GetAPIClient()
7572

73+
// Fetch and select template
74+
cobraCmd.Println("\nFetching available templates...")
75+
templateURL, templateName, err := selectTemplate(cobraCmd, apiClient)
76+
if err != nil {
77+
return fmt.Errorf("failed to select template: %w", err)
78+
}
79+
80+
cobraCmd.Printf("\nCreating application '%s'...\n", appName)
81+
7682
// Call POST /applications (token will be fetched automatically)
7783
createResp, err := apiClient.CreateApplication(appName, appDescription, orgID)
7884
if ok := api.CheckErr(cobraCmd, err); !ok {
@@ -110,7 +116,7 @@ func runCreate(cobraCmd *cobra.Command) error {
110116
cobraCmd.Printf("\nCloning template repository...\n")
111117

112118
// Clone the template repository
113-
if err := git.Clone(templateRepoURL, tempDir); err != nil {
119+
if err := git.Clone(templateURL, tempDir); err != nil {
114120
return fmt.Errorf("failed to clone template repository: %w", err)
115121
}
116122

@@ -137,7 +143,8 @@ func runCreate(cobraCmd *cobra.Command) error {
137143

138144
// Select resources for the application
139145
cobraCmd.Println("\nSelecting resources for your application...")
140-
if err := selectApplicationResources(cobraCmd, orgID, createResp.ApplicationID); err != nil {
146+
selectedResources, err := selectApplicationResources(cobraCmd, orgID, createResp.ApplicationID)
147+
if err != nil {
141148
errorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("9")) // Red
142149
cobraCmd.Println(errorStyle.Render("Failed to configure resources. Please run 'major app resources' to configure them later."))
143150
}
@@ -159,6 +166,15 @@ func runCreate(cobraCmd *cobra.Command) error {
159166
cobraCmd.Printf("\n✓ Application '%s' successfully created in ./%s\n", appName, appName)
160167
cobraCmd.Printf(" Clone URL: %s\n", cloneURL)
161168

169+
// If Vite template and resources were selected, add them using major-client
170+
if templateName == "Vite" && len(selectedResources) > 0 {
171+
cobraCmd.Println("\nAdding resources to Vite project...")
172+
if err := addResourcesToViteProject(cobraCmd, targetDir, selectedResources, createResp.ApplicationID); err != nil {
173+
cobraCmd.Printf("Warning: Failed to add resources to project: %v\n", err)
174+
cobraCmd.Println("You can manually add them later using 'pnpm clients:add'")
175+
}
176+
}
177+
162178
// Generate .env file
163179
cobraCmd.Println("\nGenerating .env file...")
164180
envFilePath, _, err := generateEnvFile(targetDir)
@@ -168,15 +184,6 @@ func runCreate(cobraCmd *cobra.Command) error {
168184
cobraCmd.Printf("✓ Generated .env file at: %s\n", envFilePath)
169185
}
170186

171-
// Generate RESOURCES.md file
172-
cobraCmd.Println("\nGenerating RESOURCES.md file...")
173-
resourcesFilePath, _, err := generateResourcesFile(targetDir)
174-
if err != nil {
175-
cobraCmd.Printf("Warning: Failed to generate RESOURCES.md file: %v\n", err)
176-
} else {
177-
cobraCmd.Printf("✓ Generated RESOURCES.md file at: %s\n", resourcesFilePath)
178-
}
179-
180187
printSuccessMessage(cobraCmd, appName)
181188

182189
return nil
@@ -251,21 +258,68 @@ func printSuccessMessage(cobraCmd *cobra.Command, appName string) {
251258
cobraCmd.Println(box)
252259
}
253260

261+
// addResourcesToViteProject adds selected resources to a Vite project using pnpm clients:add
262+
func addResourcesToViteProject(cobraCmd *cobra.Command, projectDir string, resources []api.ResourceItem, applicationID string) error {
263+
// First, install dependencies to make major-client available
264+
cobraCmd.Println(" Installing dependencies...")
265+
installCmd := exec.Command("pnpm", "install")
266+
installCmd.Dir = projectDir
267+
installCmd.Stdout = os.Stdout
268+
installCmd.Stderr = os.Stderr
269+
270+
if err := installCmd.Run(); err != nil {
271+
return fmt.Errorf("failed to install dependencies: %w", err)
272+
}
273+
274+
successCount := 0
275+
for _, resource := range resources {
276+
// Convert resource name to a valid client name (kebab-case)
277+
// The major-client tool will convert it to camelCase for the actual client
278+
clientName := resource.Name
279+
280+
cobraCmd.Printf(" Adding resource: %s (%s)...\n", resource.Name, resource.Type)
281+
282+
// Run: pnpm clients:add <resource_id> <name> <type> <description> <application_id>
283+
cmd := exec.Command("pnpm", "clients:add", resource.ID, clientName, resource.Type, resource.Description, applicationID)
284+
cmd.Dir = projectDir
285+
cmd.Stdout = os.Stdout
286+
cmd.Stderr = os.Stderr
287+
288+
if err := cmd.Run(); err != nil {
289+
cobraCmd.Printf(" ⚠ Failed to add resource %s: %v\n", resource.Name, err)
290+
continue
291+
}
292+
293+
successCount++
294+
}
295+
296+
if successCount > 0 {
297+
cobraCmd.Printf("✓ Successfully added %d/%d resource(s) to the project\n", successCount, len(resources))
298+
}
299+
300+
if successCount < len(resources) {
301+
return fmt.Errorf("failed to add %d resource(s)", len(resources)-successCount)
302+
}
303+
304+
return nil
305+
}
306+
254307
// selectApplicationResources prompts the user to select resources for the application
255-
func selectApplicationResources(cobraCmd *cobra.Command, orgID, appID string) error {
308+
// Returns the selected resources with their full details
309+
func selectApplicationResources(cobraCmd *cobra.Command, orgID, appID string) ([]api.ResourceItem, error) {
256310
// Get the API client
257311
apiClient := singletons.GetAPIClient()
258312

259313
// Fetch available resources
260314
resourcesResp, err := apiClient.GetResources(orgID)
261315
if ok := api.CheckErr(cobraCmd, err); !ok {
262-
return err
316+
return nil, err
263317
}
264318

265319
// Check if there are any resources available
266320
if len(resourcesResp.Resources) == 0 {
267321
cobraCmd.Println("No resources available in this organization.")
268-
return nil
322+
return nil, nil
269323
}
270324

271325
// Create options for the multiselect
@@ -308,22 +362,89 @@ func selectApplicationResources(cobraCmd *cobra.Command, orgID, appID string) er
308362
).WithKeyMap(customKeyMap)
309363

310364
if err := form.Run(); err != nil {
311-
return fmt.Errorf("failed to collect resource selection: %w", err)
365+
return nil, fmt.Errorf("failed to collect resource selection: %w", err)
312366
}
313367

314368
// If no resources selected, just return
315369
if len(selectedResourceIDs) == 0 {
316370
cobraCmd.Println("No resources selected.")
317-
return nil
371+
return nil, nil
318372
}
319373

320374
// Save the selected resources
321375
cobraCmd.Printf("Saving %d selected resource(s)...\n", len(selectedResourceIDs))
322376
_, err = apiClient.SaveApplicationResources(orgID, appID, selectedResourceIDs)
323377
if ok := api.CheckErr(cobraCmd, err); !ok {
324-
return err
378+
return nil, err
325379
}
326380

327381
cobraCmd.Printf("✓ Resources configured successfully\n")
328-
return nil
382+
383+
// Build and return the list of selected resources with full details
384+
var selectedResources []api.ResourceItem
385+
for _, selectedID := range selectedResourceIDs {
386+
for _, resource := range resourcesResp.Resources {
387+
if resource.ID == selectedID {
388+
selectedResources = append(selectedResources, resource)
389+
break
390+
}
391+
}
392+
}
393+
394+
return selectedResources, nil
395+
}
396+
397+
// selectTemplate prompts the user to select a template for the application
398+
// Returns the template URL and name
399+
func selectTemplate(cobraCmd *cobra.Command, apiClient *api.Client) (string, string, error) {
400+
// Fetch available templates
401+
templatesResp, err := apiClient.GetTemplates()
402+
if ok := api.CheckErr(cobraCmd, err); !ok {
403+
return "", "", err
404+
}
405+
406+
// Check if there are any templates available
407+
if len(templatesResp.Templates) == 0 {
408+
return "", "", fmt.Errorf("no templates available")
409+
}
410+
411+
// If only one template, use it automatically
412+
if len(templatesResp.Templates) == 1 {
413+
template := templatesResp.Templates[0]
414+
cobraCmd.Printf("Using template: %s\n", template.Name)
415+
return template.TemplateURL, template.Name, nil
416+
}
417+
418+
// Create options for the select
419+
options := make([]huh.Option[string], len(templatesResp.Templates))
420+
for i, template := range templatesResp.Templates {
421+
options[i] = huh.NewOption(template.Name, template.TemplateURL)
422+
}
423+
424+
// Prompt user to select a template
425+
var selectedTemplateURL string
426+
form := huh.NewForm(
427+
huh.NewGroup(
428+
huh.NewSelect[string]().
429+
Title("Select a template for your application").
430+
Description("Choose which template to use as a starting point").
431+
Options(options...).
432+
Value(&selectedTemplateURL),
433+
),
434+
)
435+
436+
if err := form.Run(); err != nil {
437+
return "", "", fmt.Errorf("failed to select template: %w", err)
438+
}
439+
440+
// Find the template name for the selected URL
441+
var selectedTemplateName string
442+
for _, template := range templatesResp.Templates {
443+
if template.TemplateURL == selectedTemplateURL {
444+
selectedTemplateName = template.Name
445+
break
446+
}
447+
}
448+
449+
return selectedTemplateURL, selectedTemplateName, nil
329450
}

0 commit comments

Comments
 (0)