@@ -127,29 +127,71 @@ func NewDeployCommand() *cli.Command {
127127 }
128128 }
129129
130- // If still no project, try to auto-detect from git remote
131- if projectID == "" {
130+ // If still no project, try auto-detect then interactive picker
131+ if projectID == "" && terminal .IsInteractive () {
132+ projects , err := client .ListProjects ()
133+ if err != nil {
134+ return err
135+ }
136+
137+ // Filter to active projects
138+ activeProjects := make ([]api.Project , 0 , len (projects ))
139+ for _ , p := range projects {
140+ if p .Status == "active" {
141+ activeProjects = append (activeProjects , p )
142+ }
143+ }
144+
145+ if len (activeProjects ) == 0 {
146+ return fmt .Errorf ("you don't have any active projects yet\n \n Create one first:\n createos projects add" )
147+ }
148+
149+ // Try to auto-detect from git remote
132150 dir , _ := os .Getwd ()
133151 repoFullName := git .GetRemoteFullName (dir )
134152 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 )
153+ for _ , p := range activeProjects {
154+ if p . Type != "vcs" && p . Type != "githubImport" {
155+ continue
156+ }
157+ var src api. VCSSource
158+ if err := json . Unmarshal ( p . Source , & src ); err != nil {
159+ continue
160+ }
161+ if src . VCSFullName == repoFullName {
162+ pterm . Info . Printf ( "Detected project %s from git remote (%s) \n " , p . DisplayName , repoFullName )
163+ useDetected , _ := pterm . DefaultInteractiveConfirm .
164+ WithDefaultText ( fmt . Sprintf ( "Deploy %s?" , p . DisplayName )).
165+ WithDefaultValue ( true ).
166+ Show ()
167+ if useDetected {
150168 projectID = p .ID
151- break
152169 }
170+ break
171+ }
172+ }
173+ }
174+
175+ // Fall back to interactive selection
176+ if projectID == "" {
177+ options := make ([]string , len (activeProjects ))
178+ for i , p := range activeProjects {
179+ options [i ] = fmt .Sprintf ("%s (%s) [%s]" , p .DisplayName , p .ID , p .Type )
180+ }
181+
182+ selected , err := pterm .DefaultInteractiveSelect .
183+ WithDefaultText ("Select a project to deploy" ).
184+ WithOptions (options ).
185+ WithFilter (true ).
186+ Show ()
187+ if err != nil {
188+ return fmt .Errorf ("selection cancelled" )
189+ }
190+
191+ for i , opt := range options {
192+ if opt == selected {
193+ projectID = activeProjects [i ].ID
194+ break
153195 }
154196 }
155197 }
@@ -217,6 +259,59 @@ func deployVCS(c *cli.Context, client *api.APIClient, project *api.Project) erro
217259 return waitForDeployment (client , project .ID , deployment )
218260}
219261
262+ // UploadDir zips a directory and uploads it as a deployment.
263+ // Exported so other packages (e.g. projects add) can trigger an upload deploy.
264+ func UploadDir (client * api.APIClient , projectID , displayName , dir string ) error {
265+ absDir , err := filepath .Abs (dir )
266+ if err != nil {
267+ return err
268+ }
269+
270+ info , err := os .Stat (absDir )
271+ if err != nil || ! info .IsDir () {
272+ return fmt .Errorf ("directory %q not found" , dir )
273+ }
274+
275+ pterm .Info .Printf ("Deploying %s from %s...\n " , displayName , absDir )
276+
277+ zipFile , err := os .CreateTemp ("" , "createos-deploy-*.zip" )
278+ if err != nil {
279+ return fmt .Errorf ("could not create temp file: %w" , err )
280+ }
281+ defer os .Remove (zipFile .Name ()) //nolint:errcheck
282+ defer zipFile .Close () //nolint:errcheck
283+
284+ spinner , _ := pterm .DefaultSpinner .Start ("Packaging files..." )
285+
286+ if err := createZip (zipFile , absDir ); err != nil {
287+ spinner .Fail ("Packaging failed" )
288+ return err
289+ }
290+
291+ stat , _ := zipFile .Stat ()
292+ if stat != nil && stat .Size () > maxZipSize {
293+ spinner .Fail ("Package too large" )
294+ return fmt .Errorf ("deployment package is %d MB (max %d MB)\n \n Tip: check that node_modules, .git, and build artifacts are excluded" ,
295+ stat .Size ()/ (1024 * 1024 ), maxZipSize / (1024 * 1024 ))
296+ }
297+
298+ spinner .UpdateText ("Uploading..." )
299+
300+ if err := zipFile .Close (); err != nil { //nolint:govet
301+ return fmt .Errorf ("could not flush deployment package: %w" , err )
302+ }
303+
304+ deployment , err := client .UploadDeploymentZip (projectID , zipFile .Name ())
305+ if err != nil {
306+ spinner .Fail ("Upload failed" )
307+ return err
308+ }
309+
310+ spinner .Success ("Uploaded" )
311+
312+ return waitForDeployment (client , projectID , deployment )
313+ }
314+
220315// deployUpload zips the local directory and uploads it.
221316func deployUpload (c * cli.Context , client * api.APIClient , project * api.Project ) error {
222317 dir := c .String ("dir" )
0 commit comments