Skip to content

Commit d19713d

Browse files
Create project on initialization
1 parent 3d89851 commit d19713d

9 files changed

Lines changed: 457 additions & 6 deletions

File tree

cmd/project.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ package cmd
33
import (
44
"errors"
55
"fmt"
6+
"strings"
67

78
"github.com/spf13/cobra"
89

910
"github.com/InitiatDev/initiat-cli/internal/client"
1011
"github.com/InitiatDev/initiat-cli/internal/config"
1112
"github.com/InitiatDev/initiat-cli/internal/env"
1213
"github.com/InitiatDev/initiat-cli/internal/project"
14+
"github.com/InitiatDev/initiat-cli/internal/prompt"
1315
"github.com/InitiatDev/initiat-cli/internal/setup"
1416
"github.com/InitiatDev/initiat-cli/internal/storage"
1517
"github.com/InitiatDev/initiat-cli/internal/table"
@@ -163,7 +165,26 @@ func runProjectInit(cmd *cobra.Command, args []string) error {
163165
c := client.New()
164166
proj, err := c.GetProjectBySlug(orgSlug, projectSlug)
165167
if err != nil {
166-
return fmt.Errorf("❌ Failed to get project info: %w", err)
168+
if errors.Is(err, client.ErrProjectNotFound) {
169+
fmt.Printf("⚠️ Project \"%s/%s\" does not exist.\n", orgSlug, projectSlug)
170+
create, promptErr := prompt.PromptYesNo("Would you like to create it?")
171+
if promptErr != nil {
172+
return fmt.Errorf("❌ Failed to read user input: %w", promptErr)
173+
}
174+
if !create {
175+
return fmt.Errorf("❌ Project creation cancelled")
176+
}
177+
178+
projectName := slugToTitle(projectSlug)
179+
fmt.Printf("Creating project \"%s\"...\n", projectCtx.String())
180+
proj, err = c.CreateProject(orgSlug, projectName, projectSlug, "")
181+
if err != nil {
182+
return fmt.Errorf("❌ Failed to create project: %w", err)
183+
}
184+
fmt.Printf("✅ Project \"%s\" created successfully!\n", projectCtx.String())
185+
} else {
186+
return fmt.Errorf("❌ Failed to get project info: %w", err)
187+
}
167188
}
168189

169190
if !checkProjectInitStatus(proj) {
@@ -204,6 +225,16 @@ func ensureInitiatFileExists(orgSlug, projectSlug string) error {
204225
return nil
205226
}
206227

228+
func slugToTitle(slug string) string {
229+
words := strings.Split(slug, "-")
230+
for i, word := range words {
231+
if len(word) > 0 {
232+
words[i] = strings.ToUpper(word[:1]) + strings.ToLower(word[1:])
233+
}
234+
}
235+
return strings.Join(words, " ")
236+
}
237+
207238
func checkProjectInitStatus(project *types.Project) bool {
208239
if project.KeyInitialized {
209240
fmt.Println("Project key already initialized on server")

cmd/project_test.go

Lines changed: 119 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,16 @@ func TestProjectInitKeyAlreadyInitialized(t *testing.T) {
217217
_ = runProjectInit(projectInitCmd, []string{})
218218
}
219219

220-
func TestProjectInitKeyNotFound(t *testing.T) {
220+
func TestProjectInitKeyNotFound_UserDeclines(t *testing.T) {
221221
capture := testutil.CaptureStdout()
222222
defer capture.Restore()
223223

224+
mock, err := testutil.MockStdin("n\n")
225+
if err != nil {
226+
t.Fatalf("Failed to mock stdin: %v", err)
227+
}
228+
defer mock.Restore()
229+
224230
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
225231
w.WriteHeader(http.StatusNotFound)
226232
w.Header().Set("Content-Type", "application/json")
@@ -231,14 +237,122 @@ func TestProjectInitKeyNotFound(t *testing.T) {
231237
setupTestEnvironment(t, server.URL)
232238

233239
projectPath = "test-org/non-existent"
234-
err := runProjectInit(projectInitCmd, []string{})
240+
err = runProjectInit(projectInitCmd, []string{})
235241
if err == nil {
236-
t.Error("Expected error for non-existent project")
242+
t.Error("Expected error when user declines to create project")
237243
return
238244
}
239-
if !strings.Contains(err.Error(), "Failed to get project info") {
240-
t.Errorf("Expected specific error message, got: %v", err)
245+
if !strings.Contains(err.Error(), "Project creation cancelled") {
246+
t.Errorf("Expected 'Project creation cancelled' error, got: %v", err)
247+
}
248+
capture.AssertContains(t, "does not exist")
249+
capture.AssertContains(t, "Would you like to create it?")
250+
}
251+
252+
func TestProjectInitKeyNotFound_UserCreates(t *testing.T) {
253+
capture := testutil.CaptureStdout()
254+
defer capture.Restore()
255+
256+
mock, err := testutil.MockStdin("y\n")
257+
if err != nil {
258+
t.Fatalf("Failed to mock stdin: %v", err)
259+
}
260+
defer mock.Restore()
261+
262+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
263+
switch r.URL.Path {
264+
case "/api/v1/projects/test-org":
265+
if r.Method == "POST" {
266+
var req types.CreateProjectRequest
267+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
268+
t.Fatalf("Failed to decode request: %v", err)
269+
}
270+
271+
projectData := types.Project{
272+
ID: 1,
273+
Name: "New Project",
274+
Slug: "new-project",
275+
CompositeSlug: "test-org/new-project",
276+
Description: "",
277+
KeyInitialized: false,
278+
KeyVersion: 0,
279+
Role: "Owner",
280+
Organization: struct {
281+
ID int `json:"id"`
282+
Name string `json:"name"`
283+
Slug string `json:"slug"`
284+
}{
285+
ID: 1,
286+
Name: "Test Org",
287+
Slug: "test-org",
288+
},
289+
}
290+
291+
response := map[string]interface{}{
292+
"success": true,
293+
"message": "Project created successfully",
294+
"data": map[string]interface{}{
295+
"project": projectData,
296+
},
297+
}
298+
299+
w.Header().Set("Content-Type", "application/json")
300+
w.WriteHeader(http.StatusCreated)
301+
json.NewEncoder(w).Encode(response)
302+
return
303+
}
304+
case "/api/v1/projects/test-org/new-project":
305+
if r.Method == "GET" {
306+
w.WriteHeader(http.StatusNotFound)
307+
w.Header().Set("Content-Type", "application/json")
308+
json.NewEncoder(w).Encode(map[string]string{"error": "project not found"})
309+
return
310+
}
311+
case "/api/v1/projects/test-org/new-project/initialize":
312+
if r.Method != "POST" {
313+
t.Errorf("Expected POST, got %s", r.Method)
314+
}
315+
316+
var req types.InitializeProjectKeyRequest
317+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
318+
t.Fatalf("Failed to decode request: %v", err)
319+
}
320+
321+
if req.WrappedProjectKey == "" {
322+
t.Error("Expected wrapped project key")
323+
}
324+
325+
if _, err := crypto.Decode(req.WrappedProjectKey); err != nil {
326+
t.Errorf("Invalid encoded wrapped key: %v", err)
327+
}
328+
329+
response := map[string]interface{}{
330+
"success": true,
331+
"message": "Project key initialized successfully",
332+
}
333+
w.Header().Set("Content-Type", "application/json")
334+
json.NewEncoder(w).Encode(response)
335+
336+
default:
337+
t.Errorf("Unexpected request path: %s", r.URL.Path)
338+
w.WriteHeader(http.StatusNotFound)
339+
}
340+
}))
341+
defer server.Close()
342+
343+
setupTestEnvironment(t, server.URL)
344+
345+
projectPath = "test-org/new-project"
346+
err = runProjectInit(projectInitCmd, []string{})
347+
if err != nil {
348+
t.Fatalf("runProjectInit failed: %v", err)
241349
}
350+
351+
capture.AssertContains(t, "does not exist")
352+
capture.AssertContains(t, "Would you like to create it?")
353+
capture.AssertContains(t, "Creating project")
354+
capture.AssertContains(t, "created successfully")
355+
capture.AssertContains(t, "initialized successfully")
242356
}
243357

244358
func setupTestEnvironment(t *testing.T, serverURL string) {

internal/client/client.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package client
33
import (
44
"crypto/ed25519"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"net/http"
89
"time"
@@ -19,6 +20,8 @@ const (
1920
debugPreviewLength = 20 // Length of key preview for debug output
2021
)
2122

23+
var ErrProjectNotFound = errors.New("project not found")
24+
2225
type Client struct {
2326
baseURL string
2427
httpClient *http.Client
@@ -151,6 +154,10 @@ func (c *Client) GetProjectBySlug(orgSlug, projectSlug string) (*types.Project,
151154
return nil, err
152155
}
153156

157+
if statusCode == http.StatusNotFound {
158+
return nil, ErrProjectNotFound
159+
}
160+
154161
var project types.Project
155162
if err := httputil.HandleGetResponse(statusCode, body, &project); err != nil {
156163
return nil, fmt.Errorf("get project failed: %w", err)
@@ -159,6 +166,32 @@ func (c *Client) GetProjectBySlug(orgSlug, projectSlug string) (*types.Project,
159166
return &project, nil
160167
}
161168

169+
func (c *Client) CreateProject(orgSlug string, name, slug, description string) (*types.Project, error) {
170+
createReq := types.CreateProjectRequest{
171+
Name: name,
172+
Slug: slug,
173+
Description: description,
174+
}
175+
176+
jsonData, err := json.Marshal(createReq)
177+
if err != nil {
178+
return nil, fmt.Errorf("failed to marshal create project request: %w", err)
179+
}
180+
181+
url := routes.BuildURL(c.baseURL, routes.Project.Create(orgSlug))
182+
statusCode, body, err := httputil.DoSignedRequest(c.httpClient, routes.POST, url, jsonData)
183+
if err != nil {
184+
return nil, err
185+
}
186+
187+
var createResp types.CreateProjectResponse
188+
if err := httputil.HandleStandardResponse(statusCode, body, &createResp); err != nil {
189+
return nil, fmt.Errorf("create project failed: %w", err)
190+
}
191+
192+
return &createResp.Project, nil
193+
}
194+
162195
func (c *Client) InitializeProjectKey(orgSlug, projectSlug string, wrappedKey []byte) error {
163196
initReq := types.InitializeProjectKeyRequest{
164197
WrappedProjectKey: crypto.Encode(wrappedKey),

0 commit comments

Comments
 (0)