Skip to content

Commit 62ce157

Browse files
fix: issue #27
1 parent 6aa3e56 commit 62ce157

4 files changed

Lines changed: 155 additions & 26 deletions

File tree

cmd/deploy/deploy.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package deploy
33

44
import (
55
"archive/zip"
6+
"encoding/json"
67
"fmt"
78
"io"
89
"os"
@@ -15,6 +16,7 @@ import (
1516

1617
"github.com/NodeOps-app/createos-cli/internal/api"
1718
"github.com/NodeOps-app/createos-cli/internal/config"
19+
"github.com/NodeOps-app/createos-cli/internal/git"
1820
"github.com/NodeOps-app/createos-cli/internal/terminal"
1921
)
2022

@@ -120,10 +122,41 @@ func NewDeployCommand() *cli.Command {
120122
if err != nil {
121123
return err
122124
}
123-
if cfg == nil {
124-
return fmt.Errorf("no project linked to this directory\n\n Link a project first:\n createos init\n\n Or specify one:\n createos deploy --project <id>")
125+
if cfg != nil {
126+
projectID = cfg.ProjectID
125127
}
126-
projectID = cfg.ProjectID
128+
}
129+
130+
// If still no project, try to auto-detect from git remote
131+
if projectID == "" {
132+
dir, _ := os.Getwd()
133+
repoFullName := git.GetRemoteFullName(dir)
134+
if repoFullName != "" {
135+
projects, err := client.ListProjects()
136+
if err == nil {
137+
for _, p := range projects {
138+
if p.Status != "active" {
139+
continue
140+
}
141+
if p.Type != "vcs" && p.Type != "githubImport" {
142+
continue
143+
}
144+
var src api.VCSSource
145+
if err := json.Unmarshal(p.Source, &src); err != nil {
146+
continue
147+
}
148+
if src.VCSFullName == repoFullName {
149+
pterm.Info.Printf("Detected project %s from git remote (%s)\n", p.DisplayName, repoFullName)
150+
projectID = p.ID
151+
break
152+
}
153+
}
154+
}
155+
}
156+
}
157+
158+
if projectID == "" {
159+
return fmt.Errorf("no project linked to this directory\n\n Link a project first:\n createos init\n\n Or specify one:\n createos deploy --project <id>")
127160
}
128161

129162
project, err := client.GetProject(projectID)

cmd/init/init.go

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package initcmd
33

44
import (
5+
"encoding/json"
56
"fmt"
67
"os"
78

@@ -10,6 +11,7 @@ import (
1011

1112
"github.com/NodeOps-app/createos-cli/internal/api"
1213
"github.com/NodeOps-app/createos-cli/internal/config"
14+
"github.com/NodeOps-app/createos-cli/internal/git"
1315
"github.com/NodeOps-app/createos-cli/internal/terminal"
1416
)
1517

@@ -75,29 +77,72 @@ func NewInitCommand() *cli.Command {
7577
return nil
7678
}
7779

78-
options := make([]string, len(projects))
79-
for i, p := range projects {
80-
desc := ""
81-
if p.Description != nil && *p.Description != "" {
82-
desc = " — " + *p.Description
80+
// Filter out non-active projects
81+
activeProjects := make([]api.Project, 0, len(projects))
82+
for _, p := range projects {
83+
if p.Status == "active" {
84+
activeProjects = append(activeProjects, p)
8385
}
84-
options[i] = fmt.Sprintf("%s (%s)%s", p.DisplayName, p.ID, desc)
8586
}
87+
projects = activeProjects
8688

87-
selected, err := pterm.DefaultInteractiveSelect.
88-
WithDefaultText("Select a project to link").
89-
WithOptions(options).
90-
Show()
91-
if err != nil {
92-
return fmt.Errorf("selection cancelled")
89+
if len(projects) == 0 {
90+
fmt.Println("You don't have any active projects yet.")
91+
return nil
9392
}
9493

95-
// Find the selected project
96-
for i, opt := range options {
97-
if opt == selected {
98-
projectID = projects[i].ID
99-
projectName = projects[i].DisplayName
100-
break
94+
// Try to auto-detect the matching VCS project from git remote
95+
repoFullName := git.GetRemoteFullName(dir)
96+
if repoFullName != "" {
97+
for _, p := range projects {
98+
if p.Type != "vcs" && p.Type != "githubImport" {
99+
continue
100+
}
101+
var src api.VCSSource
102+
if err := json.Unmarshal(p.Source, &src); err != nil {
103+
continue
104+
}
105+
if src.VCSFullName == repoFullName {
106+
pterm.Info.Printf("Detected project %s from git remote (%s)\n", p.DisplayName, repoFullName)
107+
useDetected, _ := pterm.DefaultInteractiveConfirm.
108+
WithDefaultText(fmt.Sprintf("Link to %s?", p.DisplayName)).
109+
WithDefaultValue(true).
110+
Show()
111+
if useDetected {
112+
projectID = p.ID
113+
projectName = p.DisplayName
114+
}
115+
break
116+
}
117+
}
118+
}
119+
120+
// Fall back to interactive selection if no match found
121+
if projectID == "" {
122+
options := make([]string, len(projects))
123+
for i, p := range projects {
124+
desc := ""
125+
if p.Description != nil && *p.Description != "" {
126+
desc = " — " + *p.Description
127+
}
128+
options[i] = fmt.Sprintf("%s (%s)%s", p.DisplayName, p.ID, desc)
129+
}
130+
131+
selected, err := pterm.DefaultInteractiveSelect.
132+
WithDefaultText("Select a project to link").
133+
WithOptions(options).
134+
Show()
135+
if err != nil {
136+
return fmt.Errorf("selection cancelled")
137+
}
138+
139+
// Find the selected project
140+
for i, opt := range options {
141+
if opt == selected {
142+
projectID = projects[i].ID
143+
projectName = projects[i].DisplayName
144+
break
145+
}
101146
}
102147
}
103148
}

internal/api/methods.go

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -410,10 +410,7 @@ func (c *APIClient) RetriggerDeployment(projectID, deploymentID, branch string)
410410
// TriggerLatestDeployment triggers a new deployment from the latest commit.
411411
// branch is optional — passed as a query param; omit to use the project's default branch.
412412
func (c *APIClient) TriggerLatestDeployment(projectID, branch string) (*Deployment, error) {
413-
var result Response[struct {
414-
ID string `json:"id"`
415-
}]
416-
req := c.Client.R().SetResult(&result)
413+
req := c.Client.R()
417414
if branch != "" {
418415
req = req.SetQueryParam("branch", branch)
419416
}
@@ -424,7 +421,31 @@ func (c *APIClient) TriggerLatestDeployment(projectID, branch string) (*Deployme
424421
if resp.IsError() {
425422
return nil, ParseAPIError(resp.StatusCode(), resp.Body())
426423
}
427-
return c.GetDeployment(projectID, result.Data.ID)
424+
425+
// The API returns either {"data": {"id": "..."}} for a new deployment
426+
// or {"data": "deployment already triggered"} when the latest commit is already deployed.
427+
var envelope struct {
428+
Data json.RawMessage `json:"data"`
429+
}
430+
if err := json.Unmarshal(resp.Body(), &envelope); err != nil {
431+
return nil, fmt.Errorf("unexpected response from server")
432+
}
433+
434+
// Try to parse as an object with an ID
435+
var idResp struct {
436+
ID string `json:"id"`
437+
}
438+
if err := json.Unmarshal(envelope.Data, &idResp); err == nil && idResp.ID != "" {
439+
return c.GetDeployment(projectID, idResp.ID)
440+
}
441+
442+
// Otherwise it's a plain string message (e.g. "deployment already triggered")
443+
var msg string
444+
if err := json.Unmarshal(envelope.Data, &msg); err == nil && msg != "" {
445+
return nil, fmt.Errorf("%s", msg)
446+
}
447+
448+
return nil, fmt.Errorf("unexpected response from server")
428449
}
429450

430451
// CancelDeployment cancels a running deployment.

internal/git/remote.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Package git provides helpers for interacting with the local git repository.
2+
package git
3+
4+
import (
5+
"os/exec"
6+
"regexp"
7+
"strings"
8+
)
9+
10+
// repoFullNamePattern extracts owner/repo from GitHub URLs.
11+
// Matches both HTTPS and SSH formats:
12+
// - https://github.com/owner/repo.git
13+
// - git@github.com:owner/repo.git
14+
var repoFullNamePattern = regexp.MustCompile(`github\.com[:/]([^/]+/[^/.\s]+?)(?:\.git)?$`)
15+
16+
// GetRemoteFullName returns the "owner/repo" for the git remote origin in the
17+
// given directory, or empty string if it cannot be determined.
18+
func GetRemoteFullName(dir string) string {
19+
cmd := exec.Command("git", "-C", dir, "remote", "get-url", "origin") // #nosec G204 -- dir comes from os.Getwd()
20+
out, err := cmd.Output()
21+
if err != nil {
22+
return ""
23+
}
24+
url := strings.TrimSpace(string(out))
25+
m := repoFullNamePattern.FindStringSubmatch(url)
26+
if len(m) < 2 {
27+
return ""
28+
}
29+
return m[1]
30+
}

0 commit comments

Comments
 (0)